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>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {
|
||||
PlusOutlined,
|
||||
|
|
@ -30,6 +30,30 @@ const STRATEGY_LABELS = {
|
|||
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 list = props.templateSettings?.routing?.balancers || [];
|
||||
return list.map((b, idx) => ({
|
||||
|
|
@ -83,6 +107,41 @@ function ensureBalancersArray() {
|
|||
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) {
|
||||
const out = {
|
||||
tag: form.tag,
|
||||
|
|
@ -115,6 +174,7 @@ function onConfirm(form) {
|
|||
}
|
||||
}
|
||||
}
|
||||
syncObservatories();
|
||||
modalOpen.value = false;
|
||||
}
|
||||
|
||||
|
|
@ -128,7 +188,10 @@ function confirmDelete(idx) {
|
|||
// 4 leaves the modal open if onOk returns a truthy non-thenable
|
||||
// (it expects a Promise to await), and splice() returns the array
|
||||
// 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: '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>
|
||||
|
||||
<template>
|
||||
|
|
@ -192,6 +298,16 @@ const columns = computed(() => [
|
|||
</template>
|
||||
</template>
|
||||
</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>
|
||||
|
||||
<BalancerFormModal v-model:open="modalOpen" :balancer="editingBalancer" :outbound-tags="outboundTags"
|
||||
|
|
@ -213,4 +329,10 @@ const columns = computed(() => [
|
|||
.danger {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.json-editor {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue