diff options
| author | Saeid <43953720+surbiks@users.noreply.github.com> | 2024-02-06 11:10:49 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-02-06 11:10:49 +0300 |
| commit | c53cee31f5a64ed3292f977bf5a0749324eb78a2 (patch) | |
| tree | 8a900083690e0767ee7fc2371f8203938d6e3a00 /web/html/xui | |
| parent | 222b9734caba389604fd81caa068e815bdb16dcb (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/xui')
| -rw-r--r-- | web/html/xui/xray.html | 166 | ||||
| -rw-r--r-- | web/html/xui/xray_balancer_modal.html | 111 | ||||
| -rw-r--r-- | web/html/xui/xray_rule_modal.html | 23 |
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 ( |
