mirror of
https://github.com/alireza0/x-ui.git
synced 2026-03-14 05:23:09 +00:00
[feature] add multi domain tls (CDN ready)
This commit is contained in:
@@ -92,6 +92,7 @@ docker build -t x-ui .
|
|||||||
- Search within all inbounds and clients
|
- Search within all inbounds and clients
|
||||||
- Support Dark/Light theme UI
|
- Support Dark/Light theme UI
|
||||||
- Support multi-user multi-protocol, web page visualization operation
|
- Support multi-user multi-protocol, web page visualization operation
|
||||||
|
- Support multi-domain configuration and multi-certificate inbounds
|
||||||
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
|
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
|
||||||
- Support for configuring more transport configurations
|
- Support for configuring more transport configurations
|
||||||
- Traffic statistics, limit traffic, limit expiration time
|
- Traffic statistics, limit traffic, limit expiration time
|
||||||
|
|||||||
@@ -151,9 +151,9 @@ class DBInbound {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
genLink(clientIndex) {
|
genLink(address=this.address, remark=this.remark, clientIndex=0) {
|
||||||
const inbound = this.toInbound();
|
const inbound = this.toInbound();
|
||||||
return inbound.genLink(this.address, this.remark, clientIndex);
|
return inbound.genLink(address, remark, clientIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
get genInboundLinks() {
|
get genInboundLinks() {
|
||||||
|
|||||||
@@ -494,7 +494,7 @@ class TlsStreamSettings extends XrayCommonClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!ObjectUtil.isEmpty(json.settings)) {
|
if (!ObjectUtil.isEmpty(json.settings)) {
|
||||||
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName);
|
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName, json.settings.domains);
|
||||||
}
|
}
|
||||||
return new TlsStreamSettings(
|
return new TlsStreamSettings(
|
||||||
json.serverName,
|
json.serverName,
|
||||||
@@ -562,17 +562,19 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
|||||||
};
|
};
|
||||||
|
|
||||||
TlsStreamSettings.Settings = class extends XrayCommonClass {
|
TlsStreamSettings.Settings = class extends XrayCommonClass {
|
||||||
constructor(allowInsecure = false, fingerprint = '', serverName = '') {
|
constructor(allowInsecure = false, fingerprint = '', serverName = '', domains = []) {
|
||||||
super();
|
super();
|
||||||
this.allowInsecure = allowInsecure;
|
this.allowInsecure = allowInsecure;
|
||||||
this.fingerprint = fingerprint;
|
this.fingerprint = fingerprint;
|
||||||
this.serverName = serverName;
|
this.serverName = serverName;
|
||||||
|
this.domains = domains;
|
||||||
}
|
}
|
||||||
static fromJson(json = {}) {
|
static fromJson(json = {}) {
|
||||||
return new TlsStreamSettings.Settings(
|
return new TlsStreamSettings.Settings(
|
||||||
json.allowInsecure,
|
json.allowInsecure,
|
||||||
json.fingerprint,
|
json.fingerprint,
|
||||||
json.servername,
|
json.serverName,
|
||||||
|
json.domains,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
toJson() {
|
toJson() {
|
||||||
@@ -580,6 +582,7 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
|
|||||||
allowInsecure: this.allowInsecure,
|
allowInsecure: this.allowInsecure,
|
||||||
fingerprint: this.fingerprint,
|
fingerprint: this.fingerprint,
|
||||||
serverName: this.serverName,
|
serverName: this.serverName,
|
||||||
|
domains: this.domains,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1380,24 +1383,12 @@ class Inbound extends XrayCommonClass {
|
|||||||
genLink(address='', remark='', clientIndex=0) {
|
genLink(address='', remark='', clientIndex=0) {
|
||||||
switch (this.protocol) {
|
switch (this.protocol) {
|
||||||
case Protocols.VMESS:
|
case Protocols.VMESS:
|
||||||
if (this.settings.vmesses[clientIndex].email != ""){
|
|
||||||
remark = this.settings.vmesses[clientIndex].email
|
|
||||||
}
|
|
||||||
return this.genVmessLink(address, remark, clientIndex);
|
return this.genVmessLink(address, remark, clientIndex);
|
||||||
case Protocols.VLESS:
|
case Protocols.VLESS:
|
||||||
if (this.settings.vlesses[clientIndex].email != ""){
|
|
||||||
remark = this.settings.vlesses[clientIndex].email
|
|
||||||
}
|
|
||||||
return this.genVLESSLink(address, remark, clientIndex);
|
return this.genVLESSLink(address, remark, clientIndex);
|
||||||
case Protocols.SHADOWSOCKS:
|
case Protocols.SHADOWSOCKS:
|
||||||
if (this.settings.shadowsockses[clientIndex].email != ""){
|
|
||||||
remark = this.settings.shadowsockses[clientIndex].email
|
|
||||||
}
|
|
||||||
return this.genSSLink(address, remark, clientIndex);
|
return this.genSSLink(address, remark, clientIndex);
|
||||||
case Protocols.TROJAN:
|
case Protocols.TROJAN:
|
||||||
if (this.settings.trojans[clientIndex].email != ""){
|
|
||||||
remark = this.settings.trojans[clientIndex].email
|
|
||||||
}
|
|
||||||
return this.genTrojanLink(address, remark, clientIndex);
|
return this.genTrojanLink(address, remark, clientIndex);
|
||||||
default: return '';
|
default: return '';
|
||||||
}
|
}
|
||||||
@@ -1409,12 +1400,17 @@ class Inbound extends XrayCommonClass {
|
|||||||
case Protocols.VMESS:
|
case Protocols.VMESS:
|
||||||
case Protocols.VLESS:
|
case Protocols.VLESS:
|
||||||
case Protocols.TROJAN:
|
case Protocols.TROJAN:
|
||||||
JSON.parse(this.settings).clients.forEach((_,index) => {
|
case Protocols.SHADOWSOCKS:
|
||||||
link += this.genLink(address, remark, index) + '\r\n';
|
JSON.parse(this.settings).clients.forEach((client,index) => {
|
||||||
|
if(this.tls && !ObjectUtil.isArrEmpty(this.stream.tls.settings.domains)){
|
||||||
|
this.stream.tls.settings.domains.forEach((domain) => {
|
||||||
|
link += this.genLink(domain.domain, remark + '-' + client.email + '-' + domain.remark, index) + '\r\n';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
link += this.genLink(address, remark + '-' + client.email, index) + '\r\n';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return link;
|
return link;
|
||||||
case Protocols.SHADOWSOCKS:
|
|
||||||
return (this.genSSLink(address, remark) + '\r\n');
|
|
||||||
default: return '';
|
default: return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,48 +4,54 @@
|
|||||||
:class="themeSwitcher.darkCardClass"
|
:class="themeSwitcher.darkCardClass"
|
||||||
:footer="null"
|
:footer="null"
|
||||||
width="300px">
|
width="300px">
|
||||||
<a-tag v-if="qrModal.clientName" color="orange" style="margin-bottom: 10px;display: block;text-align: center;">
|
|
||||||
{{ i18n "pages.inbounds.email" }}: "[[ qrModal.clientName ]]"
|
|
||||||
</a-tag>
|
|
||||||
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas>
|
|
||||||
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
|
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
|
||||||
|
<template v-if="app.subSettings.enable && qrModal.subId">
|
||||||
|
<a-divider>Subscription</a-divider>
|
||||||
|
<canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" style="width: 100%; height: 100%;"></canvas>
|
||||||
|
</template>
|
||||||
|
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||||
|
<template v-for="(row, index) in qrModal.qrcodes">
|
||||||
|
<a-tag color="orange" style="margin-top: 10px;display: block;text-align: center;">[[ row.remark ]]</a-tag>
|
||||||
|
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas>
|
||||||
|
</template>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
const qrModal = {
|
const qrModal = {
|
||||||
title: '',
|
title: '',
|
||||||
content: '',
|
clientIndex: 0,
|
||||||
inbound: new Inbound(),
|
inbound: new Inbound(),
|
||||||
dbInbound: new DBInbound(),
|
dbInbound: new DBInbound(),
|
||||||
copyText: '',
|
client: null,
|
||||||
clientName: null,
|
qrcodes: [],
|
||||||
qrcode: null,
|
|
||||||
clipboard: null,
|
clipboard: null,
|
||||||
visible: false,
|
visible: false,
|
||||||
show: function (title = '', content = '', dbInbound = new DBInbound(), copyText = '', clientName = null) {
|
subId: '',
|
||||||
|
show: function (title = '', dbInbound = new DBInbound(), clientIndex = 0) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.content = content;
|
this.clientIndex = clientIndex;
|
||||||
this.dbInbound = dbInbound;
|
this.dbInbound = dbInbound;
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = dbInbound.toInbound();
|
||||||
this.clientName = clientName;
|
settings = JSON.parse(this.inbound.settings);
|
||||||
if (ObjectUtil.isEmpty(copyText)) {
|
this.client = settings.clients[clientIndex];
|
||||||
this.copyText = content;
|
remark = this.dbInbound.remark + "-" + this.client.email;
|
||||||
|
address = this.dbInbound.address;
|
||||||
|
this.qrcodes = [];
|
||||||
|
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
|
||||||
|
this.inbound.stream.tls.settings.domains.forEach((domain) => {
|
||||||
|
this.qrcodes.push({
|
||||||
|
remark: remark + "-" + domain.remark,
|
||||||
|
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, clientIndex)
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.copyText = copyText;
|
this.qrcodes.push({
|
||||||
|
remark: remark,
|
||||||
|
link: this.inbound.genLink(address, remark, clientIndex)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
qrModalApp.$nextTick(() => {
|
|
||||||
if (this.qrcode === null) {
|
|
||||||
this.qrcode = new QRious({
|
|
||||||
element: document.querySelector('#qrCode'),
|
|
||||||
size: 260,
|
|
||||||
value: content,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.qrcode.value = content;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
close: function () {
|
close: function () {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
@@ -59,16 +65,40 @@
|
|||||||
qrModal: qrModal,
|
qrModal: qrModal,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
copyToClipboard() {
|
copyToClipboard(elmentId,content) {
|
||||||
this.qrModal.clipboard = new ClipboardJS('#qrCode', {
|
this.qrModal.clipboard = new ClipboardJS('#'+elmentId, {
|
||||||
text: () => this.qrModal.copyText,
|
text: () => content,
|
||||||
});
|
});
|
||||||
this.qrModal.clipboard.on('success', () => {
|
this.qrModal.clipboard.on('success', () => {
|
||||||
app.$message.success('{{ i18n "copied" }}')
|
app.$message.success('{{ i18n "copied" }}')
|
||||||
this.qrModal.clipboard.destroy();
|
this.qrModal.clipboard.destroy();
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
setQrCode(elmentId,content) {
|
||||||
|
new QRious({
|
||||||
|
element: document.querySelector('#'+elmentId),
|
||||||
|
size: 260,
|
||||||
|
value: content,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
genSubLink(subID) {
|
||||||
|
protocol = app.subSettings.tls ? "https://" : "http://";
|
||||||
|
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain;
|
||||||
|
subPort = app.subSettings.port;
|
||||||
|
port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort);
|
||||||
|
subPath = app.subSettings.path;
|
||||||
|
return protocol + hostName + port + subPath + subID;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updated() {
|
||||||
|
if (qrModal.client.subId){
|
||||||
|
qrModal.subId = qrModal.client.subId;
|
||||||
|
this.setQrCode("qrCode-sub",this.genSubLink(this.subId));
|
||||||
|
}
|
||||||
|
qrModal.qrcodes.forEach((element,index) => {
|
||||||
|
this.setQrCode("qrCode-"+index, element.link);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -65,6 +65,27 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>Multi Domain</td>
|
||||||
|
<td>
|
||||||
|
<a-switch v-model="multiDomain"></a-switch>
|
||||||
|
<a-button v-if="multiDomain" size="small" @click="inbound.stream.tls.settings.domains.push({remark: '', domain: ''})">+</a-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="multiDomain">
|
||||||
|
<td colspan="2">
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-group v-for="(row, index) in inbound.stream.tls.settings.domains">
|
||||||
|
<a-input style="width: 40%" v-model.trim="row.remark" addon-before='{{ i18n "remark" }}'></a-input>
|
||||||
|
<a-input style="width: 60%" v-model.trim="row.domain" addon-before='{{ i18n "host" }}'>
|
||||||
|
<template slot="addonAfter">
|
||||||
|
<a-button size="small" style="margin: 0px" @click="inbound.stream.tls.settings.domains.splice(index, 1)">-</a-button>
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</a-input-group>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-else>
|
||||||
<td>{{ i18n "domainName" }}</td>
|
<td>{{ i18n "domainName" }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
|
|||||||
@@ -175,10 +175,14 @@
|
|||||||
</template>
|
</template>
|
||||||
<div v-if="dbInbound.hasLink()">
|
<div v-if="dbInbound.hasLink()">
|
||||||
<a-divider>URL</a-divider>
|
<a-divider>URL</a-divider>
|
||||||
<p>[[ infoModal.link ]]</p>
|
<a-row v-for="(link,index) in infoModal.links">
|
||||||
<button class="ant-btn ant-btn-primary" id="copy-url-link" @click="copyToClipboard('copy-url-link', infoModal.link)">
|
<a-col :span="21"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
|
||||||
<a-icon type="snippets"></a-icon>{{ i18n "copy" }}
|
<a-col :span="3" style="text-align: right;">
|
||||||
</button>
|
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
|
||||||
|
<a-icon type="snippets"></a-icon>{{ i18n "copy" }}
|
||||||
|
</button>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<script>
|
<script>
|
||||||
@@ -192,7 +196,7 @@
|
|||||||
upStats: 0,
|
upStats: 0,
|
||||||
downStats: 0,
|
downStats: 0,
|
||||||
clipboard: null,
|
clipboard: null,
|
||||||
link: null,
|
links: [],
|
||||||
index: null,
|
index: null,
|
||||||
isExpired: false,
|
isExpired: false,
|
||||||
subLink: '',
|
subLink: '',
|
||||||
@@ -201,11 +205,26 @@
|
|||||||
this.index = index;
|
this.index = index;
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = dbInbound.toInbound();
|
||||||
this.dbInbound = new DBInbound(dbInbound);
|
this.dbInbound = new DBInbound(dbInbound);
|
||||||
this.link = dbInbound.genLink(index);
|
|
||||||
this.settings = JSON.parse(this.inbound.settings);
|
this.settings = JSON.parse(this.inbound.settings);
|
||||||
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
|
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
|
||||||
this.isExpired = this.inbound.isExpiry(index);
|
this.isExpired = this.inbound.isExpiry(index);
|
||||||
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
||||||
|
remark = this.dbInbound.remark + "-" + this.clientSettings.email;
|
||||||
|
address = this.dbInbound.address;
|
||||||
|
this.links = [];
|
||||||
|
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
|
||||||
|
this.inbound.stream.tls.settings.domains.forEach((domain) => {
|
||||||
|
this.links.push({
|
||||||
|
remark: remark + "-" + domain.remark,
|
||||||
|
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, index)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.links.push({
|
||||||
|
remark: remark,
|
||||||
|
link: this.inbound.genLink(address, remark, index)
|
||||||
|
});
|
||||||
|
}
|
||||||
if (this.clientSettings) {
|
if (this.clientSettings) {
|
||||||
if (this.clientSettings.subId) {
|
if (this.clientSettings.subId) {
|
||||||
this.subLink = this.genSubLink(this.clientSettings.subId);
|
this.subLink = this.genSubLink(this.clientSettings.subId);
|
||||||
|
|||||||
@@ -90,6 +90,18 @@
|
|||||||
set delayedExpireDays(days){
|
set delayedExpireDays(days){
|
||||||
this.client.expiryTime = -86400000 * days;
|
this.client.expiryTime = -86400000 * days;
|
||||||
},
|
},
|
||||||
|
get multiDomain() {
|
||||||
|
return this.inbound.stream.tls.settings.domains.length > 0;
|
||||||
|
},
|
||||||
|
set multiDomain(value) {
|
||||||
|
if (value) {
|
||||||
|
inModal.inbound.stream.tls.server = "";
|
||||||
|
inModal.inbound.stream.tls.settings.domains = [{remark: "", domain: window.location.host.split(":")[0]}];
|
||||||
|
} else {
|
||||||
|
inModal.inbound.stream.tls.server = "";
|
||||||
|
inModal.inbound.stream.tls.settings.domains = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
streamNetworkChange() {
|
streamNetworkChange() {
|
||||||
|
|||||||
@@ -104,6 +104,10 @@
|
|||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</div>
|
</div>
|
||||||
|
<a-switch v-model="enableFilter"
|
||||||
|
checked-children="{{ i18n "search" }}" un-checked-children="{{ i18n "filter" }}"
|
||||||
|
@change="toggleFilter">
|
||||||
|
</a-switch>
|
||||||
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
|
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
|
||||||
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid">
|
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid">
|
||||||
<a-radio-button value="">{{ i18n "none" }}</a-radio-button>
|
<a-radio-button value="">{{ i18n "none" }}</a-radio-button>
|
||||||
@@ -111,10 +115,6 @@
|
|||||||
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
|
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
|
||||||
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
|
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
<a-switch v-model="enableFilter"
|
|
||||||
checked-children="{{ i18n "search" }}" un-checked-children="{{ i18n "filter" }}"
|
|
||||||
@change="toggleFilter">
|
|
||||||
</a-switch>
|
|
||||||
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
||||||
:data-source="searchedInbounds"
|
:data-source="searchedInbounds"
|
||||||
:loading="spinning" :scroll="{ x: 1300 }"
|
:loading="spinning" :scroll="{ x: 1300 }"
|
||||||
@@ -760,9 +760,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
showQrcode(dbInbound, clientIndex) {
|
showQrcode(dbInbound, clientIndex) {
|
||||||
const clientName = JSON.parse(dbInbound.settings).clients[clientIndex].email;
|
qrModal.show('{{ i18n "qrCode"}}', dbInbound, clientIndex);
|
||||||
const link = dbInbound.genLink(clientIndex);
|
|
||||||
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound, '', clientName);
|
|
||||||
},
|
},
|
||||||
showInfo(dbInbound, index) {
|
showInfo(dbInbound, index) {
|
||||||
infoModal.show(dbInbound, index);
|
infoModal.show(dbInbound, index);
|
||||||
|
|||||||
Reference in New Issue
Block a user