Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/web/html
diff options
context:
space:
mode:
authorSaeid <43953720+surbiks@users.noreply.github.com>2024-02-06 11:10:49 +0300
committerGitHub <noreply@github.com>2024-02-06 11:10:49 +0300
commitc53cee31f5a64ed3292f977bf5a0749324eb78a2 (patch)
tree8a900083690e0767ee7fc2371f8203938d6e3a00 /web/html
parent222b9734caba389604fd81caa068e815bdb16dcb (diff)
Manage balancers in settings UI (#1759)
* add balancer config to ui * manage balancer in rules table * fix balancer translations * fix edit button text
Diffstat (limited to 'web/html')
-rw-r--r--web/html/xui/xray.html166
-rw-r--r--web/html/xui/xray_balancer_modal.html111
-rw-r--r--web/html/xui/xray_rule_modal.html23
3 files changed, 297 insertions, 3 deletions
diff --git a/web/html/xui/xray.html b/web/html/xui/xray.html
index 267103cb..a144c766 100644
--- a/web/html/xui/xray.html
+++ b/web/html/xui/xray.html
@@ -327,6 +327,14 @@
[[ rule.outboundTag ]]
</a-popover>
</template>
+ <template slot="balancer" slot-scope="text, rule, index">
+ <a-popover :overlay-class-name="themeSwitcher.currentTheme">
+ <template slot="content">
+ <p v-if="rule.balancerTag">Balancer Tag: [[ rule.balancerTag ]]</p>
+ </template>
+ [[ rule.balancerTag ]]
+ </a-popover>
+ </template>
<template slot="info" slot-scope="text, rule, index">
<a-popover placement="bottomRight"
v-if="(rule.source+rule.sourcePort+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0"
@@ -452,6 +460,41 @@
</template>
</a-table>
</a-tab-pane>
+ <a-tab-pane key="tpl-balancers" tab='{{ i18n "pages.xray.Balancers"}}' style="padding-top: 20px;" force-render="true">
+ <a-button type="primary" icon="plus" @click="addBalancer()" style="margin-bottom: 10px;">{{ i18n "pages.xray.balancer.addBalancer"}}</a-button>
+ <a-table :columns="balancerColumns" bordered
+ :row-key="r => r.key"
+ :data-source="balancersData"
+ :scroll="isMobile ? {} : { x: 200 }"
+ :pagination="false"
+ :indent-size="0"
+ :style="isMobile ? 'padding: 5px 0' : 'margin-left: 1px;'">
+ <template slot="action" slot-scope="text, balancer, index">
+ [[ index+1 ]]
+ <a-dropdown :trigger="['click']">
+ <a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
+ <a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
+ <a-menu-item @click="editBalancer(index)">
+ <a-icon type="edit"></a-icon>
+ {{ i18n "edit" }}
+ </a-menu-item>
+ <a-menu-item @click="deleteBalancer(index)">
+ <span style="color: #FF4D4F">
+ <a-icon type="delete"></a-icon> {{ i18n "delete"}}
+ </span>
+ </a-menu-item>
+ </a-menu>
+ </a-dropdown>
+ </template>
+ <template slot="strategy" slot-scope="text, balancer, index">
+ <a-tag style="margin:0;" v-if="balancer.strategy=='random'" color="purple">Random</a-tag>
+ <a-tag style="margin:0;" v-if="balancer.strategy=='roundRobin'" color="green">Round Robin</a-tag>
+ </template>
+ <template slot="selector" slot-scope="text, balancer, index">
+ <a-tag class="info-large-tag" style="margin:1;" v-for="sel in balancer.selector">[[ sel ]]</a-tag>
+ </template>
+ </a-table>
+ </a-tab-pane>
<a-tab-pane key="tpl-advanced" tab='{{ i18n "pages.xray.advancedTemplate"}}' style="padding-top: 20px;" force-render="true">
<a-list-item-meta title='{{ i18n "pages.xray.Template"}}' description='{{ i18n "pages.xray.TemplateDesc"}}'></a-list-item-meta>
<a-radio-group v-model="advSettings" @change="changeCode" button-style="solid" style="margin: 10px 0;" :size="isMobile ? 'small' : ''">
@@ -474,6 +517,7 @@
{{template "ruleModal"}}
{{template "outModal"}}
{{template "reverseModal"}}
+{{template "balancerModal"}}
{{template "warpModal"}}
<script>
const rulesColumns = [
@@ -490,9 +534,10 @@
{ title: 'Domain', dataIndex: 'domain', align: 'center', width: 20, ellipsis: true },
{ title: 'Port', dataIndex: 'port', align: 'center', width: 10, ellipsis: true }]},
{ title: '{{ i18n "pages.xray.rules.inbound"}}', children: [
- { title: 'Inbound Tag', dataIndex: 'inboundTag', align: 'center', width: 20, ellipsis: true },
+ { title: 'Inbound Tag', dataIndex: 'inboundTag', align: 'center', width: 15, ellipsis: true },
{ title: 'Client Email', dataIndex: 'user', align: 'center', width: 20, ellipsis: true }]},
- { title: '{{ i18n "pages.xray.rules.outbound"}}', dataIndex: 'outboundTag', align: 'center', width: 20 },
+ { title: '{{ i18n "pages.xray.rules.outbound"}}', dataIndex: 'outboundTag', align: 'center', width: 15 },
+ { title: '{{ i18n "pages.xray.rules.balancer"}}', dataIndex: 'balancerTag', align: 'center', width: 15 },
];
const rulesMobileColumns = [
@@ -517,6 +562,13 @@
{ title: '{{ i18n "pages.xray.outbound.domain"}}', dataIndex: 'domain', align: 'center', width: 50 },
];
+ const balancerColumns = [
+ { title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
+ { title: '{{ i18n "pages.xray.balancer.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
+ { title: '{{ i18n "pages.xray.balancer.balancerStrategy"}}', align: 'center', width: 50, scopedSlots: { customRender: 'strategy' }},
+ { title: '{{ i18n "pages.xray.balancer.balancerSelectors"}}', align: 'center', width: 100, scopedSlots: { customRender: 'selector' }},
+ ];
+
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
@@ -895,6 +947,95 @@
this.refreshing = false;
}
},
+ addBalancer() {
+ balancerModal.show({
+ title: '{{ i18n "pages.xray.balancer.addBalancer"}}',
+ okText: '{{ i18n "pages.xray.balancer.addBalancer"}}',
+ balancerTags: this.balancersData.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag),
+ balancer: {
+ tag: '',
+ strategy: 'random',
+ selector: []
+ },
+ confirm: (balancer) => {
+ balancerModal.loading();
+ newTemplateSettings = this.templateSettings;
+ if (newTemplateSettings.routing.balancers == undefined) {
+ newTemplateSettings.routing.balancers = [];
+ }
+ let tmpBalancer = {
+ 'tag': balancer.tag,
+ 'selector': balancer.selector
+ };
+ if (balancer.strategy == 'roundRobin') {
+ tmpBalancer.strategy = {
+ 'type': balancer.strategy
+ };
+ }
+ newTemplateSettings.routing.balancers.push(tmpBalancer);
+ this.templateSettings = newTemplateSettings;
+ balancerModal.close();
+ },
+ isEdit: false
+ });
+ },
+ editBalancer(index) {
+ const oldTag = this.balancersData[index].tag;
+ balancerModal.show({
+ title: '{{ i18n "pages.xray.balancer.editBalancer"}}',
+ okText: '{{ i18n "sure" }}',
+ balancerTags: this.balancersData.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag),
+ balancer: this.balancersData[index],
+ confirm: (balancer) => {
+ balancerModal.loading();
+ newTemplateSettings = this.templateSettings;
+
+ let tmpBalancer = {
+ 'tag': balancer.tag,
+ 'selector': balancer.selector
+ };
+ if (balancer.strategy == 'roundRobin') {
+ tmpBalancer.strategy = {
+ 'type': balancer.strategy
+ };
+ }
+
+ newTemplateSettings.routing.balancers[index] = tmpBalancer;
+ // change edited tag if used in rule section
+ if (oldTag != balancer.tag) {
+ newTemplateSettings.routing.rules.forEach((rule) => {
+ if (rule.balancerTag && rule.balancerTag == oldTag) {
+ rule.balancerTag = balancer.tag;
+ }
+ });
+ }
+ this.templateSettings = newTemplateSettings;
+ balancerModal.close();
+ },
+ isEdit: true
+ });
+ },
+ deleteBalancer(index) {
+ newTemplateSettings = this.templateSettings;
+
+ //remove from balancers
+ const oldTag = this.balancersData[index].tag;
+ this.balancersData.splice(index, 1);
+
+ // remove from settings
+ let realIndex = newTemplateSettings.routing.balancers.findIndex((b) => b.tag == oldTag);
+ newTemplateSettings.routing.balancers.splice(realIndex, 1);
+
+ // remove related routing rules
+ let rules = [];
+ newTemplateSettings.routing.rules.forEach((r) => {
+ if (!r.balancerTag || r.balancerTag != oldTag) {
+ rules.push(r);
+ }
+ });
+ newTemplateSettings.routing.rules = rules;
+ this.templateSettings = newTemplateSettings;
+ },
addReverse(){
reverseModal.show({
title: '{{ i18n "pages.xray.outbound.addReverse"}}',
@@ -1084,6 +1225,27 @@
return data;
},
},
+ balancersData: {
+ get: function () {
+ data = []
+ if (this.templateSettings != null && this.templateSettings.routing != null && this.templateSettings.routing.balancers != null) {
+ this.templateSettings.routing.balancers.forEach((o, index) => {
+ let strategy = "random"
+ if (o.strategy && o.strategy.type == "roundRobin") {
+ strategy = o.strategy.type
+ }
+
+ data.push({
+ 'key': index,
+ 'tag': o.tag ? o.tag : "",
+ 'strategy': strategy,
+ 'selector': o.selector ? o.selector : []
+ });
+ });
+ }
+ return data;
+ }
+ },
routingRuleSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
set: function (newValue) {
diff --git a/web/html/xui/xray_balancer_modal.html b/web/html/xui/xray_balancer_modal.html
new file mode 100644
index 00000000..11eea378
--- /dev/null
+++ b/web/html/xui/xray_balancer_modal.html
@@ -0,0 +1,111 @@
+{{define "balancerModal"}}
+<a-modal
+ id="balancer-modal"
+ v-model="balancerModal.visible"
+ :title="balancerModal.title"
+ @ok="balancerModal.ok"
+ :confirm-loading="balancerModal.confirmLoading"
+ :ok-button-props="{ props: { disabled: !balancerModal.isValid } }"
+ :closable="true"
+ :mask-closable="false"
+ :ok-text="balancerModal.okText"
+ cancel-text='{{ i18n "close" }}'
+ :class="themeSwitcher.currentTheme">
+ <a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
+ <a-form-item label='{{ i18n "pages.xray.balancer.tag" }}' has-feedback
+ :validate-status="balancerModal.duplicateTag? 'warning' : 'success'">
+ <a-input v-model.trim="balancerModal.balancer.tag" @change="balancerModal.check()"
+ placeholder='{{ i18n "balancerModal.balancer.tagDesc" }}'></a-input>
+ </a-form-item>
+ <a-form-item label='{{ i18n "pages.xray.balancer.balancerStrategy" }}'>
+ <a-select v-model="balancerModal.balancer.strategy" :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option value="random">Random</a-select-option>
+ <a-select-option value="roundRobin">Round Robin</a-select-option>
+ </a-select>
+ </a-form-item>
+ <a-form-item label='{{ i18n "pages.xray.balancer.balancerSelectors" }}' has-feedback :validate-status="balancerModal.emptySelector? 'warning' : 'success'">
+ <a-select v-model="balancerModal.balancer.selector" mode="tags" @change="balancerModal.checkSelector()"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option v-for="tag in balancerModal.outboundTags" :value="tag">[[ tag ]]</a-select-option>
+ </a-select>
+ </a-form-item>
+ </table>
+ </a-form>
+</a-modal>
+<script>
+ const balancerModal = {
+ title: '',
+ visible: false,
+ confirmLoading: false,
+ okText: '{{ i18n "sure" }}',
+ isEdit: false,
+ confirm: null,
+ duplicateTag: false,
+ emptySelector: false,
+ balancer: {
+ tag: '',
+ strategy: 'random',
+ selector: []
+ },
+ outboundTags: [],
+ balancerTags:[],
+ ok() {
+ if (balancerModal.balancer.selector.length == 0) {
+ balancerModal.emptySelector = true;
+ return;
+ }
+ balancerModal.emptySelector = false;
+ ObjectUtil.execute(balancerModal.confirm, balancerModal.balancer);
+ },
+ show({ title = '', okText = '{{ i18n "sure" }}', balancerTags = [], balancer, confirm = (balancer) => { }, isEdit = false }) {
+ this.title = title;
+ this.okText = okText;
+ this.confirm = confirm;
+ this.visible = true;
+ if (isEdit) {
+ balancerModal.balancer = balancer;
+ } else {
+ balancerModal.balancer = {
+ tag: '',
+ strategy: 'random',
+ selector: []
+ };
+ }
+ this.balancerTags = balancerTags.filter((tag) => tag != balancer.tag);
+ this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
+ this.isEdit = isEdit;
+ this.check()
+ },
+ close() {
+ balancerModal.visible = false;
+ balancerModal.loading(false);
+ },
+ loading(loading) {
+ balancerModal.confirmLoading = loading;
+ },
+ check() {
+ if (balancerModal.balancer.tag == '' || balancerModal.balancerTags.includes(balancerModal.balancer.tag)) {
+ this.duplicateTag = true;
+ this.isValid = false;
+ } else {
+ this.duplicateTag = false;
+ this.isValid = true;
+ }
+ },
+ checkSelector() {
+ balancerModal.emptySelector = balancerModal.balancer.selector.length == 0;
+ }
+ };
+
+ new Vue({
+ delimiters: ['[[', ']]'],
+ el: '#balancer-modal',
+ data: {
+ balancerModal: balancerModal
+ },
+ methods: {
+ }
+ });
+
+</script>
+{{end}} \ No newline at end of file
diff --git a/web/html/xui/xray_rule_modal.html b/web/html/xui/xray_rule_modal.html
index 9ed9e06a..07cc3217 100644
--- a/web/html/xui/xray_rule_modal.html
+++ b/web/html/xui/xray_rule_modal.html
@@ -107,6 +107,19 @@
<a-select-option v-for="tag in ruleModal.outboundTags" :value="tag">[[ tag ]]</a-select-option>
</a-select>
</a-form-item>
+ <a-form-item>
+ <template slot="label">
+ <a-tooltip>
+ <template slot="title">
+ <span>{{ i18n "pages.xray.balancer.balancerDesc" }}</span>
+ </template>
+ Balancer Tag <a-icon type="question-circle"></a-icon>
+ </a-tooltip>
+ </template>
+ <a-select v-model="ruleModal.rule.balancerTag" :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option v-for="tag in ruleModal.balancerTags" :value="tag">[[ tag ]]</a-select-option>
+ </a-select>
+ </a-form-item>
</table>
</a-form>
</a-modal>
@@ -133,11 +146,12 @@
protocol: [],
attrs: [],
outboundTag: "",
+ balancerTag: "",
},
inboundTags: [],
outboundTags: [],
users: [],
- balancerTag: [],
+ balancerTags: [],
ok() {
newRule = ruleModal.getResult();
ObjectUtil.execute(ruleModal.confirm, newRule);
@@ -160,6 +174,7 @@
this.rule.protocol = rule.protocol;
this.rule.attrs = rule.attrs ? Object.entries(rule.attrs) : [];
this.rule.outboundTag = rule.outboundTag;
+ this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : ""
} else {
this.rule = {
domainMatcher: "",
@@ -174,6 +189,7 @@
protocol: [],
attrs: [],
outboundTag: "",
+ balancerTag: "",
}
}
this.isEdit = isEdit;
@@ -186,6 +202,10 @@
}
if(app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag));
}
+
+ if (app.templateSettings.routing && app.templateSettings.routing.balancers) {
+ this.balancerTags = app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)
+ }
},
close() {
ruleModal.visible = false;
@@ -211,6 +231,7 @@
rule.protocol = value.protocol;
rule.attrs = Object.fromEntries(value.attrs);
rule.outboundTag = value.outboundTag;
+ rule.balancerTag = value.balancerTag;
for (const [key, value] of Object.entries(rule)) {
if (