mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-13 09:36:05 +00:00
feat(xray/balancer): restore observatory editor + auto-sync selectors
The Vue3 migration dropped the Observatory / Burst Observatory section
that used to sit under the balancer table. Without it, leastPing /
leastLoad strategies had nowhere to populate Xray's required
subjectSelector, so balancers that depended on probe data silently
ran with an empty observer config.
- Auto-seed and sync `observatory` for leastPing balancers and
`burstObservatory` for leastLoad balancers (subjectSelector
recomputed from every matching balancer's selector list). Drops
the observatory when no matching strategy remains.
- Defaults (probeURL, interval, connectivity, sampling) match the
values the legacy panel shipped, themselves taken from the Xray
docs at xtls.github.io/config/{observatory,burstobservatory}.html.
- Surface both observatories under the table as a radio-switched
JSON textarea so admins can tune probe settings inline without
dropping into the full xray template tab.
This commit is contained in:
parent
745e394c74
commit
f1760b0a28
1 changed files with 124 additions and 2 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
|
|
@ -30,6 +30,30 @@ const STRATEGY_LABELS = {
|
||||||
leastPing: 'Least ping',
|
leastPing: 'Least ping',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Observatory defaults — values that the legacy panel seeded when a
|
||||||
|
// leastPing balancer first appeared. ProbeURL / interval follow Xray's
|
||||||
|
// own docs (https://xtls.github.io/config/observatory.html).
|
||||||
|
const DEFAULT_OBSERVATORY = Object.freeze({
|
||||||
|
subjectSelector: [],
|
||||||
|
probeURL: 'https://www.google.com/generate_204',
|
||||||
|
probeInterval: '1m',
|
||||||
|
enableConcurrency: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// BurstObservatory defaults — seeded when a leastLoad balancer is
|
||||||
|
// configured. Hicloud's generate_204 is the same connectivity probe
|
||||||
|
// the legacy panel used (https://xtls.github.io/config/burstobservatory.html).
|
||||||
|
const DEFAULT_BURST_OBSERVATORY = Object.freeze({
|
||||||
|
subjectSelector: [],
|
||||||
|
pingConfig: {
|
||||||
|
destination: 'https://www.google.com/generate_204',
|
||||||
|
interval: '1m',
|
||||||
|
connectivity: 'http://connectivitycheck.platform.hicloud.com/generate_204',
|
||||||
|
timeout: '5s',
|
||||||
|
sampling: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const rows = computed(() => {
|
const rows = computed(() => {
|
||||||
const list = props.templateSettings?.routing?.balancers || [];
|
const list = props.templateSettings?.routing?.balancers || [];
|
||||||
return list.map((b, idx) => ({
|
return list.map((b, idx) => ({
|
||||||
|
|
@ -83,6 +107,41 @@ function ensureBalancersArray() {
|
||||||
return props.templateSettings.routing.balancers;
|
return props.templateSettings.routing.balancers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep observatory / burstObservatory in sync with the configured
|
||||||
|
// balancers. leastPing balancers feed Observatory's subjectSelector;
|
||||||
|
// leastLoad balancers feed BurstObservatory's. When the matching
|
||||||
|
// strategy disappears we drop the observatory entirely so the rendered
|
||||||
|
// xray config stays minimal.
|
||||||
|
function collectSelectors(list) {
|
||||||
|
const out = new Set();
|
||||||
|
list.forEach((b) => (b.selector || []).forEach((s) => s && out.add(s)));
|
||||||
|
return [...out];
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncObservatories() {
|
||||||
|
const t = props.templateSettings;
|
||||||
|
if (!t) return;
|
||||||
|
const balancers = t.routing?.balancers || [];
|
||||||
|
|
||||||
|
const leastPings = balancers.filter((b) => b.strategy?.type === 'leastPing');
|
||||||
|
if (leastPings.length > 0) {
|
||||||
|
if (!t.observatory) t.observatory = JSON.parse(JSON.stringify(DEFAULT_OBSERVATORY));
|
||||||
|
t.observatory.subjectSelector = collectSelectors(leastPings);
|
||||||
|
} else {
|
||||||
|
delete t.observatory;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leastLoads = balancers.filter((b) => b.strategy?.type === 'leastLoad');
|
||||||
|
if (leastLoads.length > 0) {
|
||||||
|
if (!t.burstObservatory) {
|
||||||
|
t.burstObservatory = JSON.parse(JSON.stringify(DEFAULT_BURST_OBSERVATORY));
|
||||||
|
}
|
||||||
|
t.burstObservatory.subjectSelector = collectSelectors(leastLoads);
|
||||||
|
} else {
|
||||||
|
delete t.burstObservatory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildWireBalancer(form) {
|
function buildWireBalancer(form) {
|
||||||
const out = {
|
const out = {
|
||||||
tag: form.tag,
|
tag: form.tag,
|
||||||
|
|
@ -115,6 +174,7 @@ function onConfirm(form) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
syncObservatories();
|
||||||
modalOpen.value = false;
|
modalOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,7 +188,10 @@ function confirmDelete(idx) {
|
||||||
// 4 leaves the modal open if onOk returns a truthy non-thenable
|
// 4 leaves the modal open if onOk returns a truthy non-thenable
|
||||||
// (it expects a Promise to await), and splice() returns the array
|
// (it expects a Promise to await), and splice() returns the array
|
||||||
// of removed items.
|
// of removed items.
|
||||||
onOk: () => { props.templateSettings.routing.balancers.splice(idx, 1); },
|
onOk: () => {
|
||||||
|
props.templateSettings.routing.balancers.splice(idx, 1);
|
||||||
|
syncObservatories();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,6 +202,49 @@ const columns = computed(() => [
|
||||||
{ title: 'Selector', key: 'selector', align: 'center' },
|
{ title: 'Selector', key: 'selector', align: 'center' },
|
||||||
{ title: 'Fallback', dataIndex: 'fallbackTag', key: 'fallbackTag', align: 'center', width: 160 },
|
{ title: 'Fallback', dataIndex: 'fallbackTag', key: 'fallbackTag', align: 'center', width: 160 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// === Observatory / BurstObservatory inline editor ====================
|
||||||
|
// The legacy panel surfaced both top-level observatory blocks here as a
|
||||||
|
// raw JSON editor so admins could tune probeURL / interval / sampling
|
||||||
|
// without having to drop into the full xray template tab. We keep that
|
||||||
|
// affordance but only render it when the matching observatory exists —
|
||||||
|
// which is itself driven by syncObservatories() above.
|
||||||
|
const hasObservatory = computed(() => !!props.templateSettings?.observatory);
|
||||||
|
const hasBurstObservatory = computed(() => !!props.templateSettings?.burstObservatory);
|
||||||
|
const showObsEditor = computed(() => hasObservatory.value || hasBurstObservatory.value);
|
||||||
|
|
||||||
|
const obsView = ref('observatory');
|
||||||
|
|
||||||
|
// Keep the radio selection valid as observatories appear/disappear —
|
||||||
|
// e.g. deleting the last leastPing balancer should flip the editor to
|
||||||
|
// the burstObservatory pane instead of leaving it pointing at the
|
||||||
|
// (now-removed) observatory key.
|
||||||
|
watch(showObsEditor, () => {
|
||||||
|
if (obsView.value === 'observatory' && !hasObservatory.value && hasBurstObservatory.value) {
|
||||||
|
obsView.value = 'burstObservatory';
|
||||||
|
} else if (obsView.value === 'burstObservatory' && !hasBurstObservatory.value && hasObservatory.value) {
|
||||||
|
obsView.value = 'observatory';
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const obsText = computed({
|
||||||
|
get: () => {
|
||||||
|
const t = props.templateSettings;
|
||||||
|
if (!t) return '';
|
||||||
|
const src = obsView.value === 'observatory' ? t.observatory : t.burstObservatory;
|
||||||
|
return src ? JSON.stringify(src, null, 2) : '';
|
||||||
|
},
|
||||||
|
set: (next) => {
|
||||||
|
let parsed;
|
||||||
|
try { parsed = JSON.parse(next); } catch (_e) { return; }
|
||||||
|
if (!props.templateSettings) return;
|
||||||
|
if (obsView.value === 'observatory') {
|
||||||
|
props.templateSettings.observatory = parsed;
|
||||||
|
} else {
|
||||||
|
props.templateSettings.burstObservatory = parsed;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -192,6 +298,16 @@ const columns = computed(() => [
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
|
|
||||||
|
<template v-if="showObsEditor">
|
||||||
|
<a-divider :style="{ margin: '8px 0' }" />
|
||||||
|
<a-radio-group v-model:value="obsView" button-style="solid" size="small">
|
||||||
|
<a-radio-button v-if="hasObservatory" value="observatory">Observatory</a-radio-button>
|
||||||
|
<a-radio-button v-if="hasBurstObservatory" value="burstObservatory">Burst Observatory</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
<a-textarea v-model:value="obsText" :auto-size="{ minRows: 8, maxRows: 24 }" spellcheck="false"
|
||||||
|
class="json-editor" />
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<BalancerFormModal v-model:open="modalOpen" :balancer="editingBalancer" :outbound-tags="outboundTags"
|
<BalancerFormModal v-model:open="modalOpen" :balancer="editingBalancer" :outbound-tags="outboundTags"
|
||||||
|
|
@ -213,4 +329,10 @@ const columns = computed(() => [
|
||||||
.danger {
|
.danger {
|
||||||
color: #ff4d4f;
|
color: #ff4d4f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.json-editor {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue