Vue.component("traffic-map-box", {
props: ["v-stats", "v-is-attack"],
mounted: function () {
this.render()
},
data: function () {
let maxPercent = 0
let isAttack = this.vIsAttack
this.vStats.forEach(function (v) {
let percent = parseFloat(v.percent)
if (percent > maxPercent) {
maxPercent = percent
}
v.formattedCountRequests = teaweb.formatCount(v.countRequests) + "次"
v.formattedCountAttackRequests = teaweb.formatCount(v.countAttackRequests) + "次"
})
if (maxPercent < 100) {
maxPercent *= 1.2 // 不要让某一项100%
}
let screenIsNarrow = window.innerWidth < 512
return {
isAttack: isAttack,
stats: this.vStats,
chart: null,
minOpacity: 0.2,
maxPercent: maxPercent,
selectedCountryName: "",
screenIsNarrow: screenIsNarrow
}
},
methods: {
render: function () {
this.chart = teaweb.initChart(document.getElementById("traffic-map-box"));
let that = this
this.chart.setOption({
backgroundColor: "white",
grid: {
top: 0,
bottom: 0,
left: 0,
right: 0
},
roam: false,
tooltip: {
trigger: "item"
},
series: [{
type: "map",
map: "world",
zoom: 1.3,
selectedMode: false,
itemStyle: {
areaColor: "#E9F0F9",
borderColor: "#DDD"
},
label: {
show: false,
fontSize: "10px",
color: "#fff",
backgroundColor: "#8B9BD3",
padding: [2, 2, 2, 2]
},
emphasis: {
itemStyle: {
areaColor: "#8B9BD3",
opacity: 1.0
},
label: {
show: true,
fontSize: "10px",
color: "#fff",
backgroundColor: "#8B9BD3",
padding: [2, 2, 2, 2]
}
},
//select: {itemStyle:{ areaColor: "#8B9BD3", opacity: 0.8 }},
tooltip: {
formatter: function (args) {
let name = args.name
let stat = null
that.stats.forEach(function (v) {
if (v.name == name) {
stat = v
}
})
if (stat != null) {
return name + " 流量:" + stat.formattedBytes + " 流量占比:" + stat.percent + "% 请求数:" + stat.formattedCountRequests + " 攻击数:" + stat.formattedCountAttackRequests
}
return name
}
},
data: this.stats.map(function (v) {
let opacity = parseFloat(v.percent) / that.maxPercent
if (opacity < that.minOpacity) {
opacity = that.minOpacity
}
let fullOpacity = opacity * 3
if (fullOpacity > 1) {
fullOpacity = 1
}
let isAttack = that.vIsAttack
let bgColor = "#276AC6"
if (isAttack) {
bgColor = "#B03A5B"
}
return {
name: v.name,
value: v.bytes,
percent: parseFloat(v.percent),
itemStyle: {
areaColor: bgColor,
opacity: opacity
},
emphasis: {
itemStyle: {
areaColor: bgColor,
opacity: fullOpacity
},
label: {
show: true,
formatter: function (args) {
return args.name
}
}
},
label: {
show: false,
formatter: function (args) {
if (args.name == that.selectedCountryName) {
return args.name
}
return ""
},
fontSize: "10px",
color: "#fff",
backgroundColor: "#8B9BD3",
padding: [2, 2, 2, 2]
}
}
}),
nameMap: window.WorldCountriesMap
}]
})
this.chart.resize()
},
selectCountry: function (countryName) {
if (this.chart == null) {
return
}
let option = this.chart.getOption()
let that = this
option.series[0].data.forEach(function (v) {
let opacity = v.percent / that.maxPercent
if (opacity < that.minOpacity) {
opacity = that.minOpacity
}
if (v.name == countryName) {
if (v.isSelected) {
v.itemStyle.opacity = opacity
v.isSelected = false
v.label.show = false
that.selectedCountryName = ""
return
}
v.isSelected = true
that.selectedCountryName = countryName
opacity *= 3
if (opacity > 1) {
opacity = 1
}
// 至少是0.5,让用户能够看清
if (opacity < 0.5) {
opacity = 0.5
}
v.itemStyle.opacity = opacity
v.label.show = true
} else {
v.itemStyle.opacity = opacity
v.isSelected = false
v.label.show = false
}
})
this.chart.setOption(option)
},
select: function (args) {
this.selectCountry(args.countryName)
}
},
template: `
`
})
Vue.component("traffic-map-box-table", {
props: ["v-stats", "v-is-attack", "v-screen-is-narrow"],
data: function () {
return {
stats: this.vStats,
isAttack: this.vIsAttack
}
},
methods: {
select: function (countryName) {
this.$emit("select", {countryName: countryName})
}
},
template: ``
})
Vue.component("ddos-protection-ports-config-box", {
props: ["v-ports"],
data: function () {
let ports = this.vPorts
if (ports == null) {
ports = []
}
return {
ports: ports,
isAdding: false,
addingPort: {
port: "",
description: ""
}
}
},
methods: {
add: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.addingPortInput.focus()
})
},
confirm: function () {
let portString = this.addingPort.port
if (portString.length == 0) {
this.warn("请输入端口号")
return
}
if (!/^\d+$/.test(portString)) {
this.warn("请输入正确的端口号")
return
}
let port = parseInt(portString, 10)
if (port <= 0) {
this.warn("请输入正确的端口号")
return
}
if (port > 65535) {
this.warn("请输入正确的端口号")
return
}
let exists = false
this.ports.forEach(function (v) {
if (v.port == port) {
exists = true
}
})
if (exists) {
this.warn("端口号已经存在")
return
}
this.ports.push({
port: port,
description: this.addingPort.description
})
this.notifyChange()
this.cancel()
},
cancel: function () {
this.isAdding = false
this.addingPort = {
port: "",
description: ""
}
},
remove: function (index) {
this.ports.$remove(index)
this.notifyChange()
},
warn: function (message) {
let that = this
teaweb.warn(message, function () {
that.$refs.addingPortInput.focus()
})
},
notifyChange: function () {
this.$emit("change", this.ports)
}
},
template: `
{{portConfig.port}}
({{portConfig.description}})
+
`
})
// 显示节点的多个集群
Vue.component("node-clusters-labels", {
props: ["v-primary-cluster", "v-secondary-clusters", "size"],
data: function () {
let cluster = this.vPrimaryCluster
let secondaryClusters = this.vSecondaryClusters
if (secondaryClusters == null) {
secondaryClusters = []
}
let labelSize = this.size
if (labelSize == null) {
labelSize = "small"
}
return {
cluster: cluster,
secondaryClusters: secondaryClusters,
labelSize: labelSize
}
},
template: ``
})
// 单个集群选择
Vue.component("cluster-selector", {
props: ["v-cluster-id"],
mounted: function () {
let that = this
Tea.action("/clusters/options")
.post()
.success(function (resp) {
that.clusters = resp.data.clusters
})
},
data: function () {
let clusterId = this.vClusterId
if (clusterId == null) {
clusterId = 0
}
return {
clusters: [],
clusterId: clusterId
}
},
template: `
[选择集群]
{{cluster.name}}
`
})
Vue.component("node-ddos-protection-config-box", {
props: ["v-ddos-protection-config", "v-default-configs", "v-is-node", "v-cluster-is-on"],
data: function () {
let config = this.vDdosProtectionConfig
if (config == null) {
config = {
tcp: {
isPrior: false,
isOn: false,
maxConnections: 0,
maxConnectionsPerIP: 0,
newConnectionsRate: 0,
newConnectionsRateBlockTimeout: 0,
newConnectionsSecondlyRate: 0,
newConnectionSecondlyRateBlockTimeout: 0,
allowIPList: [],
ports: []
}
}
}
// initialize
if (config.tcp == null) {
config.tcp = {
isPrior: false,
isOn: false,
maxConnections: 0,
maxConnectionsPerIP: 0,
newConnectionsRate: 0,
newConnectionsRateBlockTimeout: 0,
newConnectionsSecondlyRate: 0,
newConnectionSecondlyRateBlockTimeout: 0,
allowIPList: [],
ports: []
}
}
return {
config: config,
defaultConfigs: this.vDefaultConfigs,
isNode: this.vIsNode,
isAddingPort: false
}
},
methods: {
changeTCPPorts: function (ports) {
this.config.tcp.ports = ports
},
changeTCPAllowIPList: function (ipList) {
this.config.tcp.allowIPList = ipList
}
},
template: `
当前节点所在集群已设置DDoS防护。
TCP设置
启用
单节点TCP最大连接数
单IP TCP最大连接数
单IP TCP新连接速率(分钟)
单IP TCP新连接速率(秒钟)
TCP端口列表
IP白名单
`
})
Vue.component("ddos-protection-ip-list-config-box", {
props: ["v-ip-list"],
data: function () {
let list = this.vIpList
if (list == null) {
list = []
}
return {
list: list,
isAdding: false,
addingIP: {
ip: "",
description: ""
}
}
},
methods: {
add: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.addingIPInput.focus()
})
},
confirm: function () {
let ip = this.addingIP.ip
if (ip.length == 0) {
this.warn("请输入IP")
return
}
let exists = false
this.list.forEach(function (v) {
if (v.ip == ip) {
exists = true
}
})
if (exists) {
this.warn("IP '" + ip + "'已经存在")
return
}
let that = this
Tea.Vue.$post("/ui/validateIPs")
.params({
ips: [ip]
})
.success(function () {
that.list.push({
ip: ip,
description: that.addingIP.description
})
that.notifyChange()
that.cancel()
})
.fail(function () {
that.warn("请输入正确的IP")
})
},
cancel: function () {
this.isAdding = false
this.addingIP = {
ip: "",
description: ""
}
},
remove: function (index) {
this.list.$remove(index)
this.notifyChange()
},
warn: function (message) {
let that = this
teaweb.warn(message, function () {
that.$refs.addingIPInput.focus()
})
},
notifyChange: function () {
this.$emit("change", this.list)
}
},
template: `
{{ipConfig.ip}}
({{ipConfig.description}})
+
`
})
Vue.component("node-cluster-combo-box", {
props: ["v-cluster-id"],
data: function () {
let that = this
Tea.action("/clusters/options")
.post()
.success(function (resp) {
that.clusters = resp.data.clusters
})
return {
clusters: []
}
},
methods: {
change: function (item) {
if (item == null) {
this.$emit("change", 0)
} else {
this.$emit("change", item.value)
}
}
},
template: `
`
})
// 一个节点的多个集群选择器
Vue.component("node-clusters-selector", {
props: ["v-primary-cluster", "v-secondary-clusters"],
data: function () {
let primaryCluster = this.vPrimaryCluster
let secondaryClusters = this.vSecondaryClusters
if (secondaryClusters == null) {
secondaryClusters = []
}
return {
primaryClusterId: (primaryCluster == null) ? 0 : primaryCluster.id,
secondaryClusterIds: secondaryClusters.map(function (v) {
return v.id
}),
primaryCluster: primaryCluster,
secondaryClusters: secondaryClusters
}
},
methods: {
addPrimary: function () {
let that = this
let selectedClusterIds = [this.primaryClusterId].concat(this.secondaryClusterIds)
teaweb.popup("/clusters/selectPopup?selectedClusterIds=" + selectedClusterIds.join(",") + "&mode=single", {
height: "30em",
width: "50em",
callback: function (resp) {
if (resp.data.cluster != null) {
that.primaryCluster = resp.data.cluster
that.primaryClusterId = that.primaryCluster.id
that.notifyChange()
}
}
})
},
removePrimary: function () {
this.primaryClusterId = 0
this.primaryCluster = null
this.notifyChange()
},
addSecondary: function () {
let that = this
let selectedClusterIds = [this.primaryClusterId].concat(this.secondaryClusterIds)
teaweb.popup("/clusters/selectPopup?selectedClusterIds=" + selectedClusterIds.join(",") + "&mode=multiple", {
height: "30em",
width: "50em",
callback: function (resp) {
if (resp.data.cluster != null) {
that.secondaryClusterIds.push(resp.data.cluster.id)
that.secondaryClusters.push(resp.data.cluster)
that.notifyChange()
}
}
})
},
removeSecondary: function (index) {
this.secondaryClusterIds.$remove(index)
this.secondaryClusters.$remove(index)
this.notifyChange()
},
notifyChange: function () {
this.$emit("change", {
clusterId: this.primaryClusterId
})
}
},
template: ``
})
Vue.component("message-media-selector", {
props: ["v-media-type"],
mounted: function () {
let that = this
Tea.action("/admins/recipients/mediaOptions")
.post()
.success(function (resp) {
that.medias = resp.data.medias
// 初始化简介
if (that.mediaType.length > 0) {
let media = that.medias.$find(function (_, media) {
return media.type == that.mediaType
})
if (media != null) {
that.description = media.description
}
}
})
},
data: function () {
let mediaType = this.vMediaType
if (mediaType == null) {
mediaType = ""
}
return {
medias: [],
description: "",
mediaType: mediaType
}
},
watch: {
mediaType: function (v) {
let media = this.medias.$find(function (_, media) {
return media.type == v
})
if (media == null) {
this.description = ""
} else {
this.description = media.description
}
this.$emit("change", media)
},
},
template: `
[选择媒介类型]
{{media.name}}
`
})
// 消息接收人设置
Vue.component("message-receivers-box", {
props: ["v-node-cluster-id"],
mounted: function () {
let that = this
Tea.action("/clusters/cluster/settings/message/selectedReceivers")
.params({
clusterId: this.clusterId
})
.post()
.success(function (resp) {
that.receivers = resp.data.receivers
})
},
data: function () {
let clusterId = this.vNodeClusterId
if (clusterId == null) {
clusterId = 0
}
return {
clusterId: clusterId,
receivers: []
}
},
methods: {
addReceiver: function () {
let that = this
let recipientIdStrings = []
let groupIdStrings = []
this.receivers.forEach(function (v) {
if (v.type == "recipient") {
recipientIdStrings.push(v.id.toString())
} else if (v.type == "group") {
groupIdStrings.push(v.id.toString())
}
})
teaweb.popup("/clusters/cluster/settings/message/selectReceiverPopup?recipientIds=" + recipientIdStrings.join(",") + "&groupIds=" + groupIdStrings.join(","), {
callback: function (resp) {
that.receivers.push(resp.data)
}
})
},
removeReceiver: function (index) {
this.receivers.$remove(index)
}
},
template: `
分组: {{receiver.name}}
({{receiver.subName}})
+
`
})
Vue.component("message-recipient-group-selector", {
props: ["v-groups"],
data: function () {
let groups = this.vGroups
if (groups == null) {
groups = []
}
let groupIds = []
if (groups.length > 0) {
groupIds = groups.map(function (v) {
return v.id.toString()
}).join(",")
}
return {
groups: groups,
groupIds: groupIds
}
},
methods: {
addGroup: function () {
let that = this
teaweb.popup("/admins/recipients/groups/selectPopup?groupIds=" + this.groupIds, {
callback: function (resp) {
that.groups.push(resp.data.group)
that.update()
}
})
},
removeGroup: function (index) {
this.groups.$remove(index)
this.update()
},
update: function () {
let groupIds = []
if (this.groups.length > 0) {
this.groups.forEach(function (v) {
groupIds.push(v.id)
})
}
this.groupIds = groupIds.join(",")
}
},
template: ``
})
Vue.component("message-media-instance-selector", {
props: ["v-instance-id"],
mounted: function () {
let that = this
Tea.action("/admins/recipients/instances/options")
.post()
.success(function (resp) {
that.instances = resp.data.instances
// 初始化简介
if (that.instanceId > 0) {
let instance = that.instances.$find(function (_, instance) {
return instance.id == that.instanceId
})
if (instance != null) {
that.description = instance.description
that.update(instance.id)
}
}
})
},
data: function () {
let instanceId = this.vInstanceId
if (instanceId == null) {
instanceId = 0
}
return {
instances: [],
description: "",
instanceId: instanceId
}
},
watch: {
instanceId: function (v) {
this.update(v)
}
},
methods: {
update: function (v) {
let instance = this.instances.$find(function (_, instance) {
return instance.id == v
})
if (instance == null) {
this.description = ""
} else {
this.description = instance.description
}
this.$emit("change", instance)
}
},
template: `
[选择媒介]
{{instance.name}} ({{instance.media.name}})
`
})
Vue.component("message-row", {
props: ["v-message", "v-can-close"],
data: function () {
let paramsJSON = this.vMessage.params
let params = null
if (paramsJSON != null && paramsJSON.length > 0) {
params = JSON.parse(paramsJSON)
}
return {
message: this.vMessage,
params: params,
isClosing: false
}
},
methods: {
viewCert: function (certId) {
teaweb.popup("/servers/certs/certPopup?certId=" + certId, {
height: "28em",
width: "48em"
})
},
readMessage: function (messageId) {
let that = this
Tea.action("/messages/readPage")
.params({"messageIds": [messageId]})
.post()
.success(function () {
// 刷新父级页面Badge
if (window.parent.Tea != null && window.parent.Tea.Vue != null) {
window.parent.Tea.Vue.checkMessagesOnce()
}
// 刷新当前页面
if (that.vCanClose && typeof (NotifyPopup) != "undefined") {
that.isClosing = true
setTimeout(function () {
NotifyPopup({})
}, 1000)
} else {
teaweb.reload()
}
})
}
},
template: ``
})
Vue.component("ns-domain-group-selector", {
props: ["v-domain-group-id"],
data: function () {
let groupId = this.vDomainGroupId
if (groupId == null) {
groupId = 0
}
return {
userId: 0,
groupId: groupId
}
},
methods: {
change: function (group) {
if (group != null) {
this.$emit("change", group.id)
} else {
this.$emit("change", 0)
}
},
reload: function (userId) {
this.userId = userId
this.$refs.comboBox.clear()
this.$refs.comboBox.setDataURL("/ns/domains/groups/options?userId=" + userId)
this.$refs.comboBox.reloadData()
}
},
template: `
`
})
// 选择多个线路
Vue.component("ns-routes-selector", {
props: ["v-routes", "name"],
mounted: function () {
let that = this
Tea.action("/ns/routes/options")
.post()
.success(function (resp) {
that.routes = resp.data.routes
})
},
data: function () {
let selectedRoutes = this.vRoutes
if (selectedRoutes == null) {
selectedRoutes = []
}
let inputName = this.name
if (typeof inputName != "string" || inputName.length == 0) {
inputName = "routeCodes"
}
return {
routeCode: "default",
inputName: inputName,
routes: [],
isAdding: false,
routeType: "default",
selectedRoutes: selectedRoutes,
}
},
watch: {
routeType: function (v) {
this.routeCode = ""
let that = this
this.routes.forEach(function (route) {
if (route.type == v && that.routeCode.length == 0) {
that.routeCode = route.code
}
})
}
},
methods: {
add: function () {
this.isAdding = true
this.routeType = "default"
this.routeCode = "default"
this.$emit("add")
},
cancel: function () {
this.isAdding = false
this.$emit("cancel")
},
confirm: function () {
if (this.routeCode.length == 0) {
return
}
let that = this
this.routes.forEach(function (v) {
if (v.code == that.routeCode) {
that.selectedRoutes.push(v)
}
})
this.$emit("change", this.selectedRoutes)
this.cancel()
},
remove: function (index) {
this.selectedRoutes.$remove(index)
this.$emit("change", this.selectedRoutes)
}
}
,
template: `
[默认线路]
自定义线路
运营商
中国省市
全球国家地区
{{route.name}}
+
`
})
// 递归DNS设置
Vue.component("ns-recursion-config-box", {
props: ["v-recursion-config"],
data: function () {
let recursion = this.vRecursionConfig
if (recursion == null) {
recursion = {
isOn: false,
hosts: [],
allowDomains: [],
denyDomains: [],
useLocalHosts: false
}
}
if (recursion.hosts == null) {
recursion.hosts = []
}
if (recursion.allowDomains == null) {
recursion.allowDomains = []
}
if (recursion.denyDomains == null) {
recursion.denyDomains = []
}
return {
config: recursion,
hostIsAdding: false,
host: "",
updatingHost: null
}
},
methods: {
changeHosts: function (hosts) {
this.config.hosts = hosts
},
changeAllowDomains: function (domains) {
this.config.allowDomains = domains
},
changeDenyDomains: function (domains) {
this.config.denyDomains = domains
},
removeHost: function (index) {
this.config.hosts.$remove(index)
},
addHost: function () {
this.updatingHost = null
this.host = ""
this.hostIsAdding = !this.hostIsAdding
if (this.hostIsAdding) {
var that = this
setTimeout(function () {
let hostRef = that.$refs.hostRef
if (hostRef != null) {
hostRef.focus()
}
}, 200)
}
},
updateHost: function (host) {
this.updatingHost = host
this.host = host.host
this.hostIsAdding = !this.hostIsAdding
if (this.hostIsAdding) {
var that = this
setTimeout(function () {
let hostRef = that.$refs.hostRef
if (hostRef != null) {
hostRef.focus()
}
}, 200)
}
},
confirmHost: function () {
if (this.host.length == 0) {
teaweb.warn("请输入DNS地址")
return
}
// TODO 校验Host
// TODO 可以输入端口号
// TODO 可以选择协议
this.hostIsAdding = false
if (this.updatingHost == null) {
this.config.hosts.push({
host: this.host
})
} else {
this.updatingHost.host = this.host
}
},
cancelHost: function () {
this.hostIsAdding = false
}
},
template: ``
})
Vue.component("ns-access-log-ref-box", {
props: ["v-access-log-ref", "v-is-parent"],
data: function () {
let config = this.vAccessLogRef
if (config == null) {
config = {
isOn: false,
isPrior: false,
logMissingDomains: false
}
}
if (typeof (config.logMissingDomains) == "undefined") {
config.logMissingDomains = false
}
return {
config: config
}
},
template: ``
})
Vue.component("ns-node-ddos-protection-config-box", {
props: ["v-ddos-protection-config", "v-default-configs", "v-is-node", "v-cluster-is-on"],
data: function () {
let config = this.vDdosProtectionConfig
if (config == null) {
config = {
tcp: {
isPrior: false,
isOn: false,
maxConnections: 0,
maxConnectionsPerIP: 0,
newConnectionsRate: 0,
newConnectionsRateBlockTimeout: 0,
newConnectionsSecondlyRate: 0,
newConnectionSecondlyRateBlockTimeout: 0,
allowIPList: [],
ports: []
}
}
}
// initialize
if (config.tcp == null) {
config.tcp = {
isPrior: false,
isOn: false,
maxConnections: 0,
maxConnectionsPerIP: 0,
newConnectionsRate: 0,
newConnectionsRateBlockTimeout: 0,
newConnectionsSecondlyRate: 0,
newConnectionSecondlyRateBlockTimeout: 0,
allowIPList: [],
ports: []
}
}
return {
config: config,
defaultConfigs: this.vDefaultConfigs,
isNode: this.vIsNode,
isAddingPort: false
}
},
methods: {
changeTCPPorts: function (ports) {
this.config.tcp.ports = ports
},
changeTCPAllowIPList: function (ipList) {
this.config.tcp.allowIPList = ipList
}
},
template: `
当前节点所在集群已设置DDoS防护。
TCP设置
启用
单节点TCP最大连接数
单IP TCP最大连接数
单IP TCP新连接速率(分钟)
单IP TCP新连接速率(秒钟)
TCP端口列表
IP白名单
`
})
Vue.component("ns-route-ranges-box", {
props: ["v-ranges"],
data: function () {
let ranges = this.vRanges
if (ranges == null) {
ranges = []
}
return {
ranges: ranges,
isAdding: false,
isAddingBatch: false,
// 类型
rangeType: "ipRange",
isReverse: false,
// IP范围
ipRangeFrom: "",
ipRangeTo: "",
batchIPRange: "",
// CIDR
ipCIDR: "",
batchIPCIDR: "",
// region
regions: [],
regionType: "country"
}
},
methods: {
addIPRange: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.ipRangeFrom.focus()
}, 100)
},
addCIDR: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.ipCIDR.focus()
}, 100)
},
addRegions: function () {
this.isAdding = true
},
addRegion: function (regionType) {
this.regionType = regionType
},
remove: function (index) {
this.ranges.$remove(index)
},
cancelIPRange: function () {
this.isAdding = false
this.ipRangeFrom = ""
this.ipRangeTo = ""
this.isReverse = false
},
cancelIPCIDR: function () {
this.isAdding = false
this.ipCIDR = ""
this.isReverse = false
},
cancelRegions: function () {
this.isAdding = false
this.regions = []
this.regionType = "country"
this.isReverse = false
},
confirmIPRange: function () {
// 校验IP
let that = this
this.ipRangeFrom = this.ipRangeFrom.trim()
if (!this.validateIP(this.ipRangeFrom)) {
teaweb.warn("开始IP填写错误", function () {
that.$refs.ipRangeFrom.focus()
})
return
}
this.ipRangeTo = this.ipRangeTo.trim()
if (!this.validateIP(this.ipRangeTo)) {
teaweb.warn("结束IP填写错误", function () {
that.$refs.ipRangeTo.focus()
})
return
}
this.ranges.push({
type: "ipRange",
params: {
ipFrom: this.ipRangeFrom,
ipTo: this.ipRangeTo,
isReverse: this.isReverse
}
})
this.cancelIPRange()
},
confirmIPCIDR: function () {
let that = this
if (this.ipCIDR.length == 0) {
teaweb.warn("请填写CIDR", function () {
that.$refs.ipCIDR.focus()
})
return
}
if (!this.validateCIDR(this.ipCIDR)) {
teaweb.warn("请输入正确的CIDR", function () {
that.$refs.ipCIDR.focus()
})
return
}
this.ranges.push({
type: "cidr",
params: {
cidr: this.ipCIDR,
isReverse: this.isReverse
}
})
this.cancelIPCIDR()
},
confirmRegions: function () {
if (this.regions.length == 0) {
this.cancelRegions()
return
}
this.ranges.push({
type: "region",
params: {
regions: this.regions,
isReverse: this.isReverse
}
})
this.cancelRegions()
},
addBatchIPRange: function () {
this.isAddingBatch = true
let that = this
setTimeout(function () {
that.$refs.batchIPRange.focus()
}, 100)
},
addBatchCIDR: function () {
this.isAddingBatch = true
let that = this
setTimeout(function () {
that.$refs.batchIPCIDR.focus()
}, 100)
},
cancelBatchIPRange: function () {
this.isAddingBatch = false
this.batchIPRange = ""
this.isReverse = false
},
cancelBatchIPCIDR: function () {
this.isAddingBatch = false
this.batchIPCIDR = ""
this.isReverse = false
},
confirmBatchIPRange: function () {
let that = this
let rangesText = this.batchIPRange
if (rangesText.length == 0) {
teaweb.warn("请填写要加入的IP范围", function () {
that.$refs.batchIPRange.focus()
})
return
}
let validRanges = []
let invalidLine = ""
rangesText.split("\n").forEach(function (line) {
line = line.trim()
if (line.length == 0) {
return
}
line = line.replace(",", ",")
let pieces = line.split(",")
if (pieces.length != 2) {
invalidLine = line
return
}
let ipFrom = pieces[0].trim()
let ipTo = pieces[1].trim()
if (!that.validateIP(ipFrom) || !that.validateIP(ipTo)) {
invalidLine = line
return
}
validRanges.push({
type: "ipRange",
params: {
ipFrom: ipFrom,
ipTo: ipTo,
isReverse: that.isReverse
}
})
})
if (invalidLine.length > 0) {
teaweb.warn("'" + invalidLine + "'格式错误", function () {
that.$refs.batchIPRange.focus()
})
return
}
validRanges.forEach(function (v) {
that.ranges.push(v)
})
this.cancelBatchIPRange()
},
confirmBatchIPCIDR: function () {
let that = this
let rangesText = this.batchIPCIDR
if (rangesText.length == 0) {
teaweb.warn("请填写要加入的CIDR", function () {
that.$refs.batchIPCIDR.focus()
})
return
}
let validRanges = []
let invalidLine = ""
rangesText.split("\n").forEach(function (line) {
let cidr = line.trim()
if (cidr.length == 0) {
return
}
if (!that.validateCIDR(cidr)) {
invalidLine = line
return
}
validRanges.push({
type: "cidr",
params: {
cidr: cidr,
isReverse: that.isReverse
}
})
})
if (invalidLine.length > 0) {
teaweb.warn("'" + invalidLine + "'格式错误", function () {
that.$refs.batchIPCIDR.focus()
})
return
}
validRanges.forEach(function (v) {
that.ranges.push(v)
})
this.cancelBatchIPCIDR()
},
selectRegionCountry: function (country) {
if (country == null) {
return
}
this.regions.push({
type: "country",
id: country.id,
name: country.name
})
this.$refs.regionCountryComboBox.clear()
},
selectRegionProvince: function (province) {
if (province == null) {
return
}
this.regions.push({
type: "province",
id: province.id,
name: province.name
})
this.$refs.regionProvinceComboBox.clear()
},
selectRegionCity: function (city) {
if (city == null) {
return
}
this.regions.push({
type: "city",
id: city.id,
name: city.name
})
this.$refs.regionCityComboBox.clear()
},
selectRegionProvider: function (provider) {
if (provider == null) {
return
}
this.regions.push({
type: "provider",
id: provider.id,
name: provider.name
})
this.$refs.regionProviderComboBox.clear()
},
removeRegion: function (index) {
this.regions.$remove(index)
},
validateIP: function (ip) {
if (ip.length == 0) {
return
}
// IPv6
if (ip.indexOf(":") >= 0) {
let pieces = ip.split(":")
if (pieces.length > 8) {
return false
}
let isOk = true
pieces.forEach(function (piece) {
if (!/^[\da-fA-F]{0,4}$/.test(piece)) {
isOk = false
}
})
return isOk
}
if (!ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)) {
return false
}
let pieces = ip.split(".")
let isOk = true
pieces.forEach(function (v) {
let v1 = parseInt(v)
if (v1 > 255) {
isOk = false
}
})
return isOk
},
validateCIDR: function (cidr) {
let pieces = cidr.split("/")
if (pieces.length != 2) {
return false
}
let ip = pieces[0]
if (!this.validateIP(ip)) {
return false
}
let mask = pieces[1]
if (!/^\d{1,3}$/.test(mask)) {
return false
}
mask = parseInt(mask, 10)
if (cidr.indexOf(":") >= 0) { // IPv6
return mask <= 128
}
return mask <= 32
},
updateRangeType: function (rangeType) {
this.rangeType = rangeType
}
},
template: `
[排除]
IP范围:
CIDR:
区域:
{{range.params.ipFrom}} - {{range.params.ipTo}}
{{range.params.cidr}}
{{region.name}},
已添加
添加新国家/地区 省份 城市 ISP
*
添加国家/地区
添加省份
添加城市
ISP
排除
确定
添加区域
`
})
Vue.component("ns-create-records-table", {
props: ["v-types"],
data: function () {
let types = this.vTypes
if (types == null) {
types = []
}
return {
types: types,
records: [
{
name: "",
type: "A",
value: "",
routeCodes: [],
ttl: 600,
index: 0
}
],
lastIndex: 0,
isAddingRoutes: false // 是否正在添加线路
}
},
methods: {
add: function () {
this.records.push({
name: "",
type: "A",
value: "",
routeCodes: [],
ttl: 600,
index: ++this.lastIndex
})
let that = this
setTimeout(function () {
that.$refs.nameInputs.$last().focus()
}, 100)
},
remove: function (index) {
this.records.$remove(index)
},
addRoutes: function () {
this.isAddingRoutes = true
},
cancelRoutes: function () {
let that = this
setTimeout(function () {
that.isAddingRoutes = false
}, 1000)
},
changeRoutes: function (record, routes) {
if (routes == null) {
record.routeCodes = []
} else {
record.routeCodes = routes.map(function (route) {
return route.code
})
}
}
},
template: ``,
})
// 选择单一线路
Vue.component("ns-route-selector", {
props: ["v-route-code"],
mounted: function () {
let that = this
Tea.action("/ns/routes/options")
.post()
.success(function (resp) {
that.routes = resp.data.routes
})
},
data: function () {
let routeCode = this.vRouteCode
if (routeCode == null) {
routeCode = ""
}
return {
routeCode: routeCode,
routes: []
}
},
template: ``
})
Vue.component("ns-user-selector", {
props: ["v-user-id"],
data: function () {
return {}
},
methods: {
change: function (userId) {
this.$emit("change", userId)
}
},
template: `
`
})
Vue.component("ns-access-log-box", {
props: ["v-access-log", "v-keyword"],
data: function () {
let accessLog = this.vAccessLog
return {
accessLog: accessLog
}
},
methods: {
showLog: function () {
let that = this
let requestId = this.accessLog.requestId
this.$parent.$children.forEach(function (v) {
if (v.deselect != null) {
v.deselect()
}
})
this.select()
teaweb.popup("/ns/clusters/accessLogs/viewPopup?requestId=" + requestId, {
width: "50em",
height: "24em",
onClose: function () {
that.deselect()
}
})
},
select: function () {
this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)"
},
deselect: function () {
this.$refs.box.parentNode.style.cssText = ""
}
},
template: `
[{{accessLog.region}}] {{accessLog.remoteAddr}} [{{accessLog.timeLocal}}] [{{accessLog.networking}}]
{{accessLog.questionType}} {{accessLog.questionName}} ->
{{accessLog.recordType}} {{accessLog.recordValue}}
[没有记录]
线路: {{route.name}}
递归DNS
错误:[{{accessLog.error}}]
`
})
Vue.component("ns-cluster-selector", {
props: ["v-cluster-id"],
mounted: function () {
let that = this
Tea.action("/ns/clusters/options")
.post()
.success(function (resp) {
that.clusters = resp.data.clusters
})
},
data: function () {
let clusterId = this.vClusterId
if (clusterId == null) {
clusterId = 0
}
return {
clusters: [],
clusterId: clusterId
}
},
template: `
[选择集群]
{{cluster.name}}
`
})
Vue.component("ns-cluster-combo-box", {
props: ["v-cluster-id"],
data: function () {
let that = this
Tea.action("/ns/clusters/options")
.post()
.success(function (resp) {
that.clusters = resp.data.clusters
})
return {
clusters: []
}
},
methods: {
change: function (item) {
if (item == null) {
this.$emit("change", 0)
} else {
this.$emit("change", item.value)
}
}
},
template: `
`
})
Vue.component("plan-user-selector", {
props: ["v-user-id"],
data: function () {
return {}
},
methods: {
change: function (userId) {
this.$emit("change", userId)
}
},
template: `
`
})
Vue.component("plan-price-view", {
props: ["v-plan"],
data: function () {
return {
plan: this.vPlan
}
},
template: `
按时间周期计费
月度:¥{{plan.monthlyPrice}}元
季度:¥{{plan.seasonallyPrice}}元
年度:¥{{plan.yearlyPrice}}元
按流量计费
基础价格:¥{{plan.trafficPrice.base}}元/GB
按{{plan.bandwidthPrice.percentile}}th带宽计费
{{range.minMB}} - {{range.maxMB}}MB ∞ : {{range.totalPrice}}元 {{range.pricePerMB}}元/MB
`
})
Vue.component("plan-bandwidth-ranges", {
props: ["v-ranges"],
data: function () {
let ranges = this.vRanges
if (ranges == null) {
ranges = []
}
return {
ranges: ranges,
isAdding: false,
minMB: "",
maxMB: "",
pricePerMB: "",
totalPrice: "",
addingRange: {
minMB: 0,
maxMB: 0,
pricePerMB: 0,
totalPrice: 0
}
}
},
methods: {
add: function () {
this.isAdding = !this.isAdding
let that = this
setTimeout(function () {
that.$refs.minMB.focus()
})
},
cancelAdding: function () {
this.isAdding = false
},
confirm: function () {
this.isAdding = false
this.minMB = ""
this.maxMB = ""
this.pricePerMB = ""
this.totalPrice = ""
this.ranges.push(this.addingRange)
this.ranges.$sort(function (v1, v2) {
if (v1.minMB < v2.minMB) {
return -1
}
if (v1.minMB == v2.minMB) {
if (v2.maxMB == 0 || v1.maxMB < v2.maxMB) {
return -1
}
return 0
}
return 1
})
this.change()
this.addingRange = {
minMB: 0,
maxMB: 0,
pricePerMB: 0,
totalPrice: 0
}
},
remove: function (index) {
this.ranges.$remove(index)
this.change()
},
change: function () {
this.$emit("change", this.ranges)
}
},
watch: {
minMB: function (v) {
let minMB = parseInt(v.toString())
if (isNaN(minMB) || minMB < 0) {
minMB = 0
}
this.addingRange.minMB = minMB
},
maxMB: function (v) {
let maxMB = parseInt(v.toString())
if (isNaN(maxMB) || maxMB < 0) {
maxMB = 0
}
this.addingRange.maxMB = maxMB
},
pricePerMB: function (v) {
let pricePerMB = parseFloat(v.toString())
if (isNaN(pricePerMB) || pricePerMB < 0) {
pricePerMB = 0
}
this.addingRange.pricePerMB = pricePerMB
},
totalPrice: function (v) {
let totalPrice = parseFloat(v.toString())
if (isNaN(totalPrice) || totalPrice < 0) {
totalPrice = 0
}
this.addingRange.totalPrice = totalPrice
}
},
template: `
{{range.minMB}}MB -
{{range.maxMB}}MB ∞ 价格:
{{range.totalPrice}}元 {{range.pricePerMB}}元/MB
+
`
})
// 套餐价格配置
Vue.component("plan-price-config-box", {
props: ["v-price-type", "v-monthly-price", "v-seasonally-price", "v-yearly-price", "v-traffic-price", "v-bandwidth-price", "v-disable-period"],
data: function () {
let priceType = this.vPriceType
if (priceType == null) {
priceType = "bandwidth"
}
// 按时间周期计费
let monthlyPriceNumber = 0
let monthlyPrice = this.vMonthlyPrice
if (monthlyPrice == null || monthlyPrice <= 0) {
monthlyPrice = ""
} else {
monthlyPrice = monthlyPrice.toString()
monthlyPriceNumber = parseFloat(monthlyPrice)
if (isNaN(monthlyPriceNumber)) {
monthlyPriceNumber = 0
}
}
let seasonallyPriceNumber = 0
let seasonallyPrice = this.vSeasonallyPrice
if (seasonallyPrice == null || seasonallyPrice <= 0) {
seasonallyPrice = ""
} else {
seasonallyPrice = seasonallyPrice.toString()
seasonallyPriceNumber = parseFloat(seasonallyPrice)
if (isNaN(seasonallyPriceNumber)) {
seasonallyPriceNumber = 0
}
}
let yearlyPriceNumber = 0
let yearlyPrice = this.vYearlyPrice
if (yearlyPrice == null || yearlyPrice <= 0) {
yearlyPrice = ""
} else {
yearlyPrice = yearlyPrice.toString()
yearlyPriceNumber = parseFloat(yearlyPrice)
if (isNaN(yearlyPriceNumber)) {
yearlyPriceNumber = 0
}
}
// 按流量计费
let trafficPrice = this.vTrafficPrice
let trafficPriceBaseNumber = 0
if (trafficPrice != null) {
trafficPriceBaseNumber = trafficPrice.base
} else {
trafficPrice = {
base: 0
}
}
let trafficPriceBase = ""
if (trafficPriceBaseNumber > 0) {
trafficPriceBase = trafficPriceBaseNumber.toString()
}
// 按带宽计费
let bandwidthPrice = this.vBandwidthPrice
if (bandwidthPrice == null) {
bandwidthPrice = {
percentile: 95,
ranges: []
}
} else if (bandwidthPrice.ranges == null) {
bandwidthPrice.ranges = []
}
return {
priceType: priceType,
monthlyPrice: monthlyPrice,
seasonallyPrice: seasonallyPrice,
yearlyPrice: yearlyPrice,
monthlyPriceNumber: monthlyPriceNumber,
seasonallyPriceNumber: seasonallyPriceNumber,
yearlyPriceNumber: yearlyPriceNumber,
trafficPriceBase: trafficPriceBase,
trafficPrice: trafficPrice,
bandwidthPrice: bandwidthPrice,
bandwidthPercentile: bandwidthPrice.percentile
}
},
methods: {
changeBandwidthPriceRanges: function (ranges) {
this.bandwidthPrice.ranges = ranges
}
},
watch: {
monthlyPrice: function (v) {
let price = parseFloat(v)
if (isNaN(price)) {
price = 0
}
this.monthlyPriceNumber = price
},
seasonallyPrice: function (v) {
let price = parseFloat(v)
if (isNaN(price)) {
price = 0
}
this.seasonallyPriceNumber = price
},
yearlyPrice: function (v) {
let price = parseFloat(v)
if (isNaN(price)) {
price = 0
}
this.yearlyPriceNumber = price
},
trafficPriceBase: function (v) {
let price = parseFloat(v)
if (isNaN(price)) {
price = 0
}
this.trafficPrice.base = price
},
bandwidthPercentile: function (v) {
let percentile = parseInt(v)
if (isNaN(percentile) || percentile <= 0) {
percentile = 95
} else if (percentile > 100) {
percentile = 100
}
this.bandwidthPrice.percentile = percentile
}
},
template: ``
})
Vue.component("http-stat-config-box", {
props: ["v-stat-config", "v-is-location", "v-is-group"],
data: function () {
let stat = this.vStatConfig
if (stat == null) {
stat = {
isPrior: false,
isOn: false
}
}
return {
stat: stat
}
},
template: ``
})
Vue.component("http-request-conds-box", {
props: ["v-conds"],
data: function () {
let conds = this.vConds
if (conds == null) {
conds = {
isOn: true,
connector: "or",
groups: []
}
}
if (conds.groups == null) {
conds.groups = []
}
return {
conds: conds,
components: window.REQUEST_COND_COMPONENTS
}
},
methods: {
change: function () {
this.$emit("change", this.conds)
},
addGroup: function () {
window.UPDATING_COND_GROUP = null
let that = this
teaweb.popup("/servers/server/settings/conds/addGroupPopup", {
height: "30em",
callback: function (resp) {
that.conds.groups.push(resp.data.group)
that.change()
}
})
},
updateGroup: function (groupIndex, group) {
window.UPDATING_COND_GROUP = group
let that = this
teaweb.popup("/servers/server/settings/conds/addGroupPopup", {
height: "30em",
callback: function (resp) {
Vue.set(that.conds.groups, groupIndex, resp.data.group)
that.change()
}
})
},
removeGroup: function (groupIndex) {
let that = this
teaweb.confirm("确定要删除这一组条件吗?", function () {
that.conds.groups.$remove(groupIndex)
that.change()
})
},
typeName: function (cond) {
let c = this.components.$find(function (k, v) {
return v.type == cond.type
})
if (c != null) {
return c.name;
}
return cond.param + " " + cond.operator
}
},
template: `
分组{{groupIndex+1}}
{{cond.param}} {{cond.operator}}
{{typeName(cond)}}:
{{cond.value}}
{{group.connector}}
+添加分组
`
})
Vue.component("ssl-config-box", {
props: ["v-ssl-policy", "v-protocol", "v-server-id"],
created: function () {
let that = this
setTimeout(function () {
that.sortableCipherSuites()
}, 100)
},
data: function () {
let policy = this.vSslPolicy
if (policy == null) {
policy = {
id: 0,
isOn: true,
certRefs: [],
certs: [],
clientCARefs: [],
clientCACerts: [],
clientAuthType: 0,
minVersion: "TLS 1.1",
hsts: null,
cipherSuitesIsOn: false,
cipherSuites: [],
http2Enabled: true,
ocspIsOn: false
}
} else {
if (policy.certRefs == null) {
policy.certRefs = []
}
if (policy.certs == null) {
policy.certs = []
}
if (policy.clientCARefs == null) {
policy.clientCARefs = []
}
if (policy.clientCACerts == null) {
policy.clientCACerts = []
}
if (policy.cipherSuites == null) {
policy.cipherSuites = []
}
}
let hsts = policy.hsts
if (hsts == null) {
hsts = {
isOn: false,
maxAge: 31536000,
includeSubDomains: false,
preload: false,
domains: []
}
}
return {
policy: policy,
// hsts
hsts: hsts,
hstsOptionsVisible: false,
hstsDomainAdding: false,
addingHstsDomain: "",
hstsDomainEditingIndex: -1,
// 相关数据
allVersions: window.SSL_ALL_VERSIONS,
allCipherSuites: window.SSL_ALL_CIPHER_SUITES.$copy(),
modernCipherSuites: window.SSL_MODERN_CIPHER_SUITES,
intermediateCipherSuites: window.SSL_INTERMEDIATE_CIPHER_SUITES,
allClientAuthTypes: window.SSL_ALL_CLIENT_AUTH_TYPES,
cipherSuitesVisible: false,
// 高级选项
moreOptionsVisible: false
}
},
watch: {
hsts: {
deep: true,
handler: function () {
this.policy.hsts = this.hsts
}
}
},
methods: {
// 删除证书
removeCert: function (index) {
let that = this
teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前服务不再使用此证书。", function () {
that.policy.certRefs.$remove(index)
that.policy.certs.$remove(index)
})
},
// 选择证书
selectCert: function () {
let that = this
let selectedCertIds = []
if (this.policy != null && this.policy.certs.length > 0) {
this.policy.certs.forEach(function (cert) {
selectedCertIds.push(cert.id.toString())
})
}
teaweb.popup("/servers/certs/selectPopup?selectedCertIds=" + selectedCertIds, {
width: "50em",
height: "30em",
callback: function (resp) {
that.policy.certRefs.push(resp.data.certRef)
that.policy.certs.push(resp.data.cert)
}
})
},
// 上传证书
uploadCert: function () {
let that = this
teaweb.popup("/servers/certs/uploadPopup", {
height: "28em",
callback: function (resp) {
teaweb.success("上传成功", function () {
that.policy.certRefs.push(resp.data.certRef)
that.policy.certs.push(resp.data.cert)
})
}
})
},
// 申请证书
requestCert: function () {
// 已经在证书中的域名
let excludeServerNames = []
if (this.policy != null && this.policy.certs.length > 0) {
this.policy.certs.forEach(function (cert) {
excludeServerNames.$pushAll(cert.dnsNames)
})
}
let that = this
teaweb.popup("/servers/server/settings/https/requestCertPopup?serverId=" + this.vServerId + "&excludeServerNames=" + excludeServerNames.join(","), {
callback: function () {
that.policy.certRefs.push(resp.data.certRef)
that.policy.certs.push(resp.data.cert)
}
})
},
// 更多选项
changeOptionsVisible: function () {
this.moreOptionsVisible = !this.moreOptionsVisible
},
// 格式化时间
formatTime: function (timestamp) {
return new Date(timestamp * 1000).format("Y-m-d")
},
// 格式化加密套件
formatCipherSuite: function (cipherSuite) {
return cipherSuite.replace(/(AES|3DES)/, "$1 ")
},
// 添加单个套件
addCipherSuite: function (cipherSuite) {
if (!this.policy.cipherSuites.$contains(cipherSuite)) {
this.policy.cipherSuites.push(cipherSuite)
}
this.allCipherSuites.$removeValue(cipherSuite)
},
// 删除单个套件
removeCipherSuite: function (cipherSuite) {
let that = this
teaweb.confirm("确定要删除此套件吗?", function () {
that.policy.cipherSuites.$removeValue(cipherSuite)
that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$findAll(function (k, v) {
return !that.policy.cipherSuites.$contains(v)
})
})
},
// 清除所选套件
clearCipherSuites: function () {
let that = this
teaweb.confirm("确定要清除所有已选套件吗?", function () {
that.policy.cipherSuites = []
that.allCipherSuites = window.SSL_ALL_CIPHER_SUITES.$copy()
})
},
// 批量添加套件
addBatchCipherSuites: function (suites) {
var that = this
teaweb.confirm("确定要批量添加套件?", function () {
suites.$each(function (k, v) {
if (that.policy.cipherSuites.$contains(v)) {
return
}
that.policy.cipherSuites.push(v)
})
})
},
/**
* 套件拖动排序
*/
sortableCipherSuites: function () {
var box = document.querySelector(".cipher-suites-box")
Sortable.create(box, {
draggable: ".label",
handle: ".icon.handle",
onStart: function () {
},
onUpdate: function (event) {
}
})
},
// 显示所有套件
showAllCipherSuites: function () {
this.cipherSuitesVisible = !this.cipherSuitesVisible
},
// 显示HSTS更多选项
showMoreHSTS: function () {
this.hstsOptionsVisible = !this.hstsOptionsVisible;
if (this.hstsOptionsVisible) {
this.changeHSTSMaxAge()
}
},
// 监控HSTS有效期修改
changeHSTSMaxAge: function () {
var v = this.hsts.maxAge
if (isNaN(v)) {
this.hsts.days = "-"
return
}
this.hsts.days = parseInt(v / 86400)
if (isNaN(this.hsts.days)) {
this.hsts.days = "-"
} else if (this.hsts.days < 0) {
this.hsts.days = "-"
}
},
// 设置HSTS有效期
setHSTSMaxAge: function (maxAge) {
this.hsts.maxAge = maxAge
this.changeHSTSMaxAge()
},
// 添加HSTS域名
addHstsDomain: function () {
this.hstsDomainAdding = true
this.hstsDomainEditingIndex = -1
let that = this
setTimeout(function () {
that.$refs.addingHstsDomain.focus()
}, 100)
},
// 修改HSTS域名
editHstsDomain: function (index) {
this.hstsDomainEditingIndex = index
this.addingHstsDomain = this.hsts.domains[index]
this.hstsDomainAdding = true
let that = this
setTimeout(function () {
that.$refs.addingHstsDomain.focus()
}, 100)
},
// 确认HSTS域名添加
confirmAddHstsDomain: function () {
this.addingHstsDomain = this.addingHstsDomain.trim()
if (this.addingHstsDomain.length == 0) {
return;
}
if (this.hstsDomainEditingIndex > -1) {
this.hsts.domains[this.hstsDomainEditingIndex] = this.addingHstsDomain
} else {
this.hsts.domains.push(this.addingHstsDomain)
}
this.cancelHstsDomainAdding()
},
// 取消HSTS域名添加
cancelHstsDomainAdding: function () {
this.hstsDomainAdding = false
this.addingHstsDomain = ""
this.hstsDomainEditingIndex = -1
},
// 删除HSTS域名
removeHstsDomain: function (index) {
this.cancelHstsDomainAdding()
this.hsts.domains.$remove(index)
},
// 选择客户端CA证书
selectClientCACert: function () {
let that = this
teaweb.popup("/servers/certs/selectPopup?isCA=1", {
width: "50em",
height: "30em",
callback: function (resp) {
that.policy.clientCARefs.push(resp.data.certRef)
that.policy.clientCACerts.push(resp.data.cert)
}
})
},
// 上传CA证书
uploadClientCACert: function () {
let that = this
teaweb.popup("/servers/certs/uploadPopup?isCA=1", {
height: "28em",
callback: function (resp) {
teaweb.success("上传成功", function () {
that.policy.clientCARefs.push(resp.data.certRef)
that.policy.clientCACerts.push(resp.data.cert)
})
}
})
},
// 删除客户端CA证书
removeClientCACert: function (index) {
let that = this
teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前服务不再使用此证书。", function () {
that.policy.clientCARefs.$remove(index)
that.policy.clientCACerts.$remove(index)
})
}
},
template: ``
})
// Action列表
Vue.component("http-firewall-actions-view", {
props: ["v-actions"],
template: `
{{action.name}} ({{action.code.toUpperCase()}})
`
})
Vue.component("http-request-scripts-config-box", {
props: ["vRequestScriptsConfig", "v-is-location"],
data: function () {
let config = this.vRequestScriptsConfig
if (config == null) {
config = {}
}
return {
config: config
}
},
methods: {
changeInitGroup: function (group) {
this.config.initGroup = group
this.$forceUpdate()
},
changeRequestGroup: function (group) {
this.config.requestGroup = group
this.$forceUpdate()
}
},
template: ``
})
// 显示WAF规则的标签
Vue.component("http-firewall-rule-label", {
props: ["v-rule"],
data: function () {
return {
rule: this.vRule
}
},
methods: {
showErr: function (err) {
teaweb.popupTip("规则校验错误,请修正:" + teaweb.encodeHTML(err) + " ")
},
},
template: `
{{rule.name}}[{{rule.param}}]
{{rule.checkpointOptions.period}}秒/{{rule.checkpointOptions.threshold}}请求
{{rule.checkpointOptions.allowDomains}}
| {{paramFilter.code}}
{{rule.operator}}
{{rule.value}}
({{rule.description}})
规则错误
`
})
// 缓存条件列表
Vue.component("http-cache-refs-box", {
props: ["v-cache-refs"],
data: function () {
let refs = this.vCacheRefs
if (refs == null) {
refs = []
}
return {
refs: refs
}
},
methods: {
timeUnitName: function (unit) {
switch (unit) {
case "ms":
return "毫秒"
case "second":
return "秒"
case "minute":
return "分钟"
case "hour":
return "小时"
case "day":
return "天"
case "week":
return "周 "
}
return unit
}
},
template: `
缓存条件
分组关系
缓存时间
{{cacheRef.minSize.count}}{{cacheRef.minSize.unit}}
- {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}}
0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}}
{{cacheRef.methods.join(", ")}}
Expires
状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}}
区间缓存
If-None-Match
If-Modified-Since
和
或
{{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}}
不缓存
`
})
Vue.component("ssl-certs-box", {
props: [
"v-certs", // 证书列表
"v-cert", // 单个证书
"v-protocol", // 协议:https|tls
"v-view-size", // 弹窗尺寸:normal, mini
"v-single-mode", // 单证书模式
"v-description" // 描述文字
],
data: function () {
let certs = this.vCerts
if (certs == null) {
certs = []
}
if (this.vCert != null) {
certs.push(this.vCert)
}
let description = this.vDescription
if (description == null || typeof (description) != "string") {
description = ""
}
return {
certs: certs,
description: description
}
},
methods: {
certIds: function () {
return this.certs.map(function (v) {
return v.id
})
},
// 删除证书
removeCert: function (index) {
let that = this
teaweb.confirm("确定删除此证书吗?证书数据仍然保留,只是当前服务不再使用此证书。", function () {
that.certs.$remove(index)
})
},
// 选择证书
selectCert: function () {
let that = this
let width = "50em"
let height = "30em"
let viewSize = this.vViewSize
if (viewSize == null) {
viewSize = "normal"
}
if (viewSize == "mini") {
width = "35em"
height = "20em"
}
teaweb.popup("/servers/certs/selectPopup?viewSize=" + viewSize, {
width: width,
height: height,
callback: function (resp) {
that.certs.push(resp.data.cert)
}
})
},
// 上传证书
uploadCert: function () {
let that = this
teaweb.popup("/servers/certs/uploadPopup", {
height: "28em",
callback: function (resp) {
teaweb.success("上传成功", function () {
that.certs.push(resp.data.cert)
})
}
})
},
// 格式化时间
formatTime: function (timestamp) {
return new Date(timestamp * 1000).format("Y-m-d")
},
// 判断是否显示选择|上传按钮
buttonsVisible: function () {
return this.vSingleMode == null || !this.vSingleMode || this.certs == null || this.certs.length == 0
}
},
template: `
{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}
选择或上传证书后HTTPS TLS 服务才能生效。
{{description}}
选择已有证书
上传新证书
`
})
Vue.component("http-host-redirect-box", {
props: ["v-redirects"],
mounted: function () {
let that = this
sortTable(function (ids) {
let newRedirects = []
ids.forEach(function (id) {
that.redirects.forEach(function (redirect) {
if (redirect.id == id) {
newRedirects.push(redirect)
}
})
})
that.updateRedirects(newRedirects)
})
},
data: function () {
let redirects = this.vRedirects
if (redirects == null) {
redirects = []
}
let id = 0
redirects.forEach(function (v) {
id++
v.id = id
})
return {
redirects: redirects,
statusOptions: [
{"code": 301, "text": "Moved Permanently"},
{"code": 308, "text": "Permanent Redirect"},
{"code": 302, "text": "Found"},
{"code": 303, "text": "See Other"},
{"code": 307, "text": "Temporary Redirect"}
],
id: id
}
},
methods: {
add: function () {
let that = this
window.UPDATING_REDIRECT = null
teaweb.popup("/servers/server/settings/redirects/createPopup", {
width: "50em",
height: "30em",
callback: function (resp) {
that.id++
resp.data.redirect.id = that.id
that.redirects.push(resp.data.redirect)
that.change()
}
})
},
update: function (index, redirect) {
let that = this
window.UPDATING_REDIRECT = redirect
teaweb.popup("/servers/server/settings/redirects/createPopup", {
width: "50em",
height: "30em",
callback: function (resp) {
resp.data.redirect.id = redirect.id
Vue.set(that.redirects, index, resp.data.redirect)
that.change()
}
})
},
remove: function (index) {
let that = this
teaweb.confirm("确定要删除这条跳转规则吗?", function () {
that.redirects.$remove(index)
that.change()
})
},
change: function () {
let that = this
setTimeout(function (){
that.$emit("change", that.redirects)
}, 100)
},
updateRedirects: function (newRedirects) {
this.redirects = newRedirects
this.change()
}
},
template: `
[创建]
跳转前URL
跳转后URL
匹配模式
HTTP状态码
状态
操作
{{redirect.beforeURL}}
匹配条件
->
{{redirect.afterURL}}
匹配前缀
正则匹配
精准匹配
{{redirect.status}}
默认
修改
删除
`
})
// 单个缓存条件设置
Vue.component("http-cache-ref-box", {
props: ["v-cache-ref", "v-is-reverse"],
mounted: function () {
this.$refs.variablesDescriber.update(this.ref.key)
if (this.ref.simpleCond != null) {
this.condType = this.ref.simpleCond.type
this.changeCondType(this.ref.simpleCond.type, true)
this.condCategory = "simple"
} else if (this.ref.conds != null && this.ref.conds.groups != null) {
this.condCategory = "complex"
}
this.changeCondCategory(this.condCategory)
},
data: function () {
let ref = this.vCacheRef
if (ref == null) {
ref = {
isOn: true,
cachePolicyId: 0,
key: "${scheme}://${host}${requestPath}${isArgs}${args}",
life: {count: 2, unit: "hour"},
status: [200],
maxSize: {count: 32, unit: "mb"},
minSize: {count: 0, unit: "kb"},
skipCacheControlValues: ["private", "no-cache", "no-store"],
skipSetCookie: true,
enableRequestCachePragma: false,
conds: null, // 复杂条件
simpleCond: null, // 简单条件
allowChunkedEncoding: true,
allowPartialContent: false,
enableIfNoneMatch: false,
enableIfModifiedSince: false,
isReverse: this.vIsReverse,
methods: [],
expiresTime: {
isPrior: false,
isOn: false,
overwrite: true,
autoCalculate: true,
duration: {count: -1, "unit": "hour"}
}
}
}
if (ref.key == null) {
ref.key = ""
}
if (ref.methods == null) {
ref.methods = []
}
if (ref.life == null) {
ref.life = {count: 2, unit: "hour"}
}
if (ref.maxSize == null) {
ref.maxSize = {count: 32, unit: "mb"}
}
if (ref.minSize == null) {
ref.minSize = {count: 0, unit: "kb"}
}
let condType = "url-extension"
let condComponent = window.REQUEST_COND_COMPONENTS.$find(function (k, v) {
return v.type == "url-extension"
})
return {
ref: ref,
moreOptionsVisible: false,
condCategory: "simple", // 条件分类:simple|complex
condType: condType,
condComponent: condComponent,
condIsCaseInsensitive: (ref.simpleCond != null) ? ref.simpleCond.isCaseInsensitive : true,
components: window.REQUEST_COND_COMPONENTS
}
},
methods: {
changeOptionsVisible: function (v) {
this.moreOptionsVisible = v
},
changeLife: function (v) {
this.ref.life = v
},
changeMaxSize: function (v) {
this.ref.maxSize = v
},
changeMinSize: function (v) {
this.ref.minSize = v
},
changeConds: function (v) {
this.ref.conds = v
this.ref.simpleCond = null
},
changeStatusList: function (list) {
let result = []
list.forEach(function (status) {
let statusNumber = parseInt(status)
if (isNaN(statusNumber) || statusNumber < 100 || statusNumber > 999) {
return
}
result.push(statusNumber)
})
this.ref.status = result
},
changeMethods: function (methods) {
this.ref.methods = methods.map(function (v) {
return v.toUpperCase()
})
},
changeKey: function (key) {
this.$refs.variablesDescriber.update(key)
},
changeExpiresTime: function (expiresTime) {
this.ref.expiresTime = expiresTime
},
// 切换条件类型
changeCondCategory: function (condCategory) {
this.condCategory = condCategory
// resize window
let dialog = window.parent.document.querySelector("*[role='dialog']")
switch (condCategory) {
case "simple":
dialog.style.width = "40em"
break
case "complex":
let width = window.parent.innerWidth
if (width > 1024) {
width = 1024
}
dialog.style.width = width + "px"
break
}
},
changeCondType: function (condType, isInit) {
if (!isInit && this.ref.simpleCond != null) {
this.ref.simpleCond.value = null
}
let def = this.components.$find(function (k, component) {
return component.type == condType
})
if (def != null) {
this.condComponent = def
}
}
},
template: `
条件类型 *
URL扩展名
URL前缀
首页
URL完整路径
URL正则匹配
参数匹配
{{condComponent.paramsTitle}} *
不区分大小写
匹配条件分组 *
缓存有效期 *
缓存Key *
请求方法限制
客户端过期时间(Expires)
可缓存的最大内容尺寸
可缓存的最小内容尺寸
支持分片内容
支持缓存区间内容
状态码列表
跳过的Cache-Control值
跳过Set-Cookie
支持请求no-cache刷新
允许If-None-Match回源
允许If-Modified-Since回源
`
})
// 请求限制
Vue.component("http-request-limit-config-box", {
props: ["v-request-limit-config", "v-is-group", "v-is-location"],
data: function () {
let config = this.vRequestLimitConfig
if (config == null) {
config = {
isPrior: false,
isOn: false,
maxConns: 0,
maxConnsPerIP: 0,
maxBodySize: {
count: -1,
unit: "kb"
},
outBandwidthPerConn: {
count: -1,
unit: "kb"
}
}
}
return {
config: config,
maxConns: config.maxConns,
maxConnsPerIP: config.maxConnsPerIP
}
},
watch: {
maxConns: function (v) {
let conns = parseInt(v, 10)
if (isNaN(conns)) {
this.config.maxConns = 0
return
}
if (conns < 0) {
this.config.maxConns = 0
} else {
this.config.maxConns = conns
}
},
maxConnsPerIP: function (v) {
let conns = parseInt(v, 10)
if (isNaN(conns)) {
this.config.maxConnsPerIP = 0
return
}
if (conns < 0) {
this.config.maxConnsPerIP = 0
} else {
this.config.maxConnsPerIP = conns
}
}
},
methods: {
isOn: function () {
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
}
},
template: ``
})
Vue.component("http-header-replace-values", {
props: ["v-replace-values"],
data: function () {
let values = this.vReplaceValues
if (values == null) {
values = []
}
return {
values: values,
isAdding: false,
addingValue: {"pattern": "", "replacement": "", "isCaseInsensitive": false, "isRegexp": false}
}
},
methods: {
add: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.pattern.focus()
})
},
remove: function (index) {
this.values.$remove(index)
},
confirm: function () {
let that = this
if (this.addingValue.pattern.length == 0) {
teaweb.warn("替换前内容不能为空", function () {
that.$refs.pattern.focus()
})
return
}
this.values.push(this.addingValue)
this.cancel()
},
cancel: function () {
this.isAdding = false
this.addingValue = {"pattern": "", "replacement": "", "isCaseInsensitive": false, "isRegexp": false}
}
},
template: `
{{value.pattern}} =>
{{value.replacement}} [空]
+
`
})
// 浏览条件列表
Vue.component("http-request-conds-view", {
props: ["v-conds"],
data: function () {
let conds = this.vConds
if (conds == null) {
conds = {
isOn: true,
connector: "or",
groups: []
}
}
if (conds.groups == null) {
conds.groups = []
}
let that = this
conds.groups.forEach(function (group) {
group.conds.forEach(function (cond) {
cond.typeName = that.typeName(cond)
})
})
return {
initConds: conds
}
},
computed: {
// 之所以使用computed,是因为需要动态更新
conds: function () {
return this.initConds
}
},
methods: {
typeName: function (cond) {
let c = window.REQUEST_COND_COMPONENTS.$find(function (k, v) {
return v.type == cond.type
})
if (c != null) {
return c.name;
}
return cond.param + " " + cond.operator
},
updateConds: function (conds) {
this.initConds = conds
},
notifyChange: function () {
let that = this
if (this.initConds.groups != null) {
this.initConds.groups.forEach(function (group) {
group.conds.forEach(function (cond) {
cond.typeName = that.typeName(cond)
})
})
this.$forceUpdate()
}
}
},
template: `
{{cond.param}} {{cond.operator}}
{{cond.typeName}}:
{{cond.value}}
{{group.connector}}
{{group.description}}
`
})
Vue.component("http-firewall-config-box", {
props: ["v-firewall-config", "v-is-location", "v-is-group", "v-firewall-policy"],
data: function () {
let firewall = this.vFirewallConfig
if (firewall == null) {
firewall = {
isPrior: false,
isOn: false,
firewallPolicyId: 0
}
}
return {
firewall: firewall
}
},
template: ``
})
// 指标图表
Vue.component("metric-chart", {
props: ["v-chart", "v-stats", "v-item"],
mounted: function () {
this.load()
},
data: function () {
let stats = this.vStats
if (stats == null) {
stats = []
}
if (stats.length > 0) {
let sum = stats.$sum(function (k, v) {
return v.value
})
if (sum < stats[0].total) {
if (this.vChart.type == "pie") {
stats.push({
keys: ["其他"],
value: stats[0].total - sum,
total: stats[0].total,
time: stats[0].time
})
}
}
}
if (this.vChart.maxItems > 0) {
stats = stats.slice(0, this.vChart.maxItems)
} else {
stats = stats.slice(0, 10)
}
stats.$rsort(function (v1, v2) {
return v1.value - v2.value
})
let widthPercent = 100
if (this.vChart.widthDiv > 0) {
widthPercent = 100 / this.vChart.widthDiv
}
return {
chart: this.vChart,
stats: stats,
item: this.vItem,
width: widthPercent + "%",
chartId: "metric-chart-" + this.vChart.id,
valueTypeName: (this.vItem != null && this.vItem.valueTypeName != null && this.vItem.valueTypeName.length > 0) ? this.vItem.valueTypeName : ""
}
},
methods: {
load: function () {
var el = document.getElementById(this.chartId)
if (el == null || el.offsetWidth == 0 || el.offsetHeight == 0) {
setTimeout(this.load, 100)
} else {
this.render(el)
}
},
render: function (el) {
let chart = echarts.init(el)
window.addEventListener("resize", function () {
chart.resize()
})
switch (this.chart.type) {
case "pie":
this.renderPie(chart)
break
case "bar":
this.renderBar(chart)
break
case "timeBar":
this.renderTimeBar(chart)
break
case "timeLine":
this.renderTimeLine(chart)
break
case "table":
this.renderTable(chart)
break
}
},
renderPie: function (chart) {
let values = this.stats.map(function (v) {
return {
name: v.keys[0],
value: v.value
}
})
let that = this
chart.setOption({
tooltip: {
show: true,
trigger: "item",
formatter: function (data) {
let stat = that.stats[data.dataIndex]
let percent = 0
if (stat.total > 0) {
percent = Math.round((stat.value * 100 / stat.total) * 100) / 100
}
let value = stat.value
switch (that.item.valueType) {
case "byte":
value = teaweb.formatBytes(value)
break
case "count":
value = teaweb.formatNumber(value)
break
}
return stat.keys[0] + " " + that.valueTypeName + ": " + value + " 占比:" + percent + "%"
}
},
series: [
{
name: name,
type: "pie",
data: values,
areaStyle: {},
color: ["#9DD3E8", "#B2DB9E", "#F39494", "#FBD88A", "#879BD7"]
}
]
})
},
renderTimeBar: function (chart) {
this.stats.$sort(function (v1, v2) {
return (v1.time < v2.time) ? -1 : 1
})
let values = this.stats.map(function (v) {
return v.value
})
let axis = {unit: "", divider: 1}
switch (this.item.valueType) {
case "count":
axis = teaweb.countAxis(values, function (v) {
return v
})
break
case "byte":
axis = teaweb.bytesAxis(values, function (v) {
return v
})
break
}
let that = this
chart.setOption({
xAxis: {
data: this.stats.map(function (v) {
return that.formatTime(v.time)
})
},
yAxis: {
axisLabel: {
formatter: function (value) {
return value + axis.unit
}
}
},
tooltip: {
show: true,
trigger: "item",
formatter: function (data) {
let stat = that.stats[data.dataIndex]
let value = stat.value
switch (that.item.valueType) {
case "byte":
value = teaweb.formatBytes(value)
break
}
return that.formatTime(stat.time) + ": " + value
}
},
grid: {
left: 50,
top: 10,
right: 20,
bottom: 25
},
series: [
{
name: name,
type: "bar",
data: values.map(function (v) {
return v / axis.divider
}),
itemStyle: {
color: teaweb.DefaultChartColor
},
areaStyle: {},
barWidth: "20em"
}
]
})
},
renderTimeLine: function (chart) {
this.stats.$sort(function (v1, v2) {
return (v1.time < v2.time) ? -1 : 1
})
let values = this.stats.map(function (v) {
return v.value
})
let axis = {unit: "", divider: 1}
switch (this.item.valueType) {
case "count":
axis = teaweb.countAxis(values, function (v) {
return v
})
break
case "byte":
axis = teaweb.bytesAxis(values, function (v) {
return v
})
break
}
let that = this
chart.setOption({
xAxis: {
data: this.stats.map(function (v) {
return that.formatTime(v.time)
})
},
yAxis: {
axisLabel: {
formatter: function (value) {
return value + axis.unit
}
}
},
tooltip: {
show: true,
trigger: "item",
formatter: function (data) {
let stat = that.stats[data.dataIndex]
let value = stat.value
switch (that.item.valueType) {
case "byte":
value = teaweb.formatBytes(value)
break
}
return that.formatTime(stat.time) + ": " + value
}
},
grid: {
left: 50,
top: 10,
right: 20,
bottom: 25
},
series: [
{
name: name,
type: "line",
data: values.map(function (v) {
return v / axis.divider
}),
itemStyle: {
color: teaweb.DefaultChartColor
},
areaStyle: {}
}
]
})
},
renderBar: function (chart) {
let values = this.stats.map(function (v) {
return v.value
})
let axis = {unit: "", divider: 1}
switch (this.item.valueType) {
case "count":
axis = teaweb.countAxis(values, function (v) {
return v
})
break
case "byte":
axis = teaweb.bytesAxis(values, function (v) {
return v
})
break
}
let bottom = 24
let rotate = 0
let result = teaweb.xRotation(chart, this.stats.map(function (v) {
return v.keys[0]
}))
if (result != null) {
bottom = result[0]
rotate = result[1]
}
let that = this
chart.setOption({
xAxis: {
data: this.stats.map(function (v) {
return v.keys[0]
}),
axisLabel: {
interval: 0,
rotate: rotate
}
},
tooltip: {
show: true,
trigger: "item",
formatter: function (data) {
let stat = that.stats[data.dataIndex]
let percent = 0
if (stat.total > 0) {
percent = Math.round((stat.value * 100 / stat.total) * 100) / 100
}
let value = stat.value
switch (that.item.valueType) {
case "byte":
value = teaweb.formatBytes(value)
break
case "count":
value = teaweb.formatNumber(value)
break
}
return stat.keys[0] + " " + that.valueTypeName + ":" + value + " 占比:" + percent + "%"
}
},
yAxis: {
axisLabel: {
formatter: function (value) {
return value + axis.unit
}
}
},
grid: {
left: 40,
top: 10,
right: 20,
bottom: bottom
},
series: [
{
name: name,
type: "bar",
data: values.map(function (v) {
return v / axis.divider
}),
itemStyle: {
color: teaweb.DefaultChartColor
},
areaStyle: {},
barWidth: "20em"
}
]
})
if (this.item.keys != null) {
// IP相关操作
if (this.item.keys.$contains("${remoteAddr}")) {
let that = this
chart.on("click", function (args) {
let index = that.item.keys.$indexesOf("${remoteAddr}")[0]
let value = that.stats[args.dataIndex].keys[index]
teaweb.popup("/servers/ipbox?ip=" + value, {
width: "50em",
height: "30em"
})
})
}
}
},
renderTable: function (chart) {
let table = `
对象
数值
占比
`
let that = this
this.stats.forEach(function (v) {
let value = v.value
switch (that.item.valueType) {
case "byte":
value = teaweb.formatBytes(value)
break
}
table += "" + v.keys[0] + " " + value + " "
let percent = 0
if (v.total > 0) {
percent = Math.round((v.value * 100 / v.total) * 100) / 100
}
table += "" + percent + "% "
table += " "
})
table += `
`
document.getElementById(this.chartId).innerHTML = table
},
formatTime: function (time) {
if (time == null) {
return ""
}
switch (this.item.periodUnit) {
case "month":
return time.substring(0, 4) + "-" + time.substring(4, 6)
case "week":
return time.substring(0, 4) + "-" + time.substring(4, 6)
case "day":
return time.substring(0, 4) + "-" + time.substring(4, 6) + "-" + time.substring(6, 8)
case "hour":
return time.substring(0, 4) + "-" + time.substring(4, 6) + "-" + time.substring(6, 8) + " " + time.substring(8, 10)
case "minute":
return time.substring(0, 4) + "-" + time.substring(4, 6) + "-" + time.substring(6, 8) + " " + time.substring(8, 10) + ":" + time.substring(10, 12)
}
return time
}
},
template: `
{{chart.name}} ({{valueTypeName}})
`
})
Vue.component("metric-board", {
template: `
`
})
Vue.component("http-cache-config-box", {
props: ["v-cache-config", "v-is-location", "v-is-group", "v-cache-policy", "v-web-id"],
data: function () {
let cacheConfig = this.vCacheConfig
if (cacheConfig == null) {
cacheConfig = {
isPrior: false,
isOn: false,
addStatusHeader: true,
addAgeHeader: false,
enableCacheControlMaxAge: false,
cacheRefs: [],
purgeIsOn: false,
purgeKey: "",
disablePolicyRefs: false
}
}
if (cacheConfig.cacheRefs == null) {
cacheConfig.cacheRefs = []
}
return {
cacheConfig: cacheConfig,
moreOptionsVisible: false,
enablePolicyRefs: !cacheConfig.disablePolicyRefs
}
},
watch: {
enablePolicyRefs: function (v) {
this.cacheConfig.disablePolicyRefs = !v
}
},
methods: {
isOn: function () {
return ((!this.vIsLocation && !this.vIsGroup) || this.cacheConfig.isPrior) && this.cacheConfig.isOn
},
isPlus: function () {
return Tea.Vue.teaIsPlus
},
generatePurgeKey: function () {
let r = Math.random().toString() + Math.random().toString()
let s = r.replace(/0\./g, "")
.replace(/\./g, "")
let result = ""
for (let i = 0; i < s.length; i++) {
result += String.fromCharCode(parseInt(s.substring(i, i + 1)) + ((Math.random() < 0.5) ? "a" : "A").charCodeAt(0))
}
this.cacheConfig.purgeKey = result
},
showMoreOptions: function () {
this.moreOptionsVisible = !this.moreOptionsVisible
},
changeStale: function (stale) {
this.cacheConfig.stale = stale
}
},
template: ``
})
// 通用Header长度
let defaultGeneralHeaders = ["Cache-Control", "Connection", "Date", "Pragma", "Trailer", "Transfer-Encoding", "Upgrade", "Via", "Warning"]
Vue.component("http-cond-general-header-length", {
props: ["v-checkpoint"],
data: function () {
let headers = null
let length = null
if (window.parent.UPDATING_RULE != null) {
let options = window.parent.UPDATING_RULE.checkpointOptions
if (options.headers != null && Array.$isArray(options.headers)) {
headers = options.headers
}
if (options.length != null) {
length = options.length
}
}
if (headers == null) {
headers = defaultGeneralHeaders
}
if (length == null) {
length = 128
}
let that = this
setTimeout(function () {
that.change()
}, 100)
return {
headers: headers,
length: length
}
},
watch: {
length: function (v) {
let len = parseInt(v)
if (isNaN(len)) {
len = 0
}
if (len < 0) {
len = 0
}
this.length = len
this.change()
}
},
methods: {
change: function () {
this.vCheckpoint.options = [
{
code: "headers",
value: this.headers
},
{
code: "length",
value: this.length
}
]
}
},
template: ``
})
// CC
Vue.component("http-firewall-checkpoint-cc", {
props: ["v-checkpoint"],
data: function () {
let keys = []
let period = 60
let threshold = 1000
let ignoreCommonFiles = false
let options = {}
if (window.parent.UPDATING_RULE != null) {
options = window.parent.UPDATING_RULE.checkpointOptions
}
if (options == null) {
options = {}
}
if (options.keys != null) {
keys = options.keys
}
if (keys.length == 0) {
keys = ["${remoteAddr}", "${requestPath}"]
}
if (options.period != null) {
period = options.period
}
if (options.threshold != null) {
threshold = options.threshold
}
if (options.ignoreCommonFiles != null && typeof (options.ignoreCommonFiles) == "boolean") {
ignoreCommonFiles = options.ignoreCommonFiles
}
let that = this
setTimeout(function () {
that.change()
}, 100)
return {
keys: keys,
period: period,
threshold: threshold,
ignoreCommonFiles: ignoreCommonFiles,
options: {},
value: threshold
}
},
watch: {
period: function () {
this.change()
},
threshold: function () {
this.change()
},
ignoreCommonFiles: function () {
this.change()
}
},
methods: {
changeKeys: function (keys) {
this.keys = keys
this.change()
},
change: function () {
let period = parseInt(this.period.toString())
if (isNaN(period) || period <= 0) {
period = 60
}
let threshold = parseInt(this.threshold.toString())
if (isNaN(threshold) || threshold <= 0) {
threshold = 1000
}
this.value = threshold
let ignoreCommonFiles = this.ignoreCommonFiles
if (typeof ignoreCommonFiles != "boolean") {
ignoreCommonFiles = false
}
this.vCheckpoint.options = [
{
code: "keys",
value: this.keys
},
{
code: "period",
value: period,
},
{
code: "threshold",
value: threshold
},
{
code: "ignoreCommonFiles",
value: ignoreCommonFiles
}
]
},
thresholdTooLow: function () {
let threshold = parseInt(this.threshold.toString())
if (isNaN(threshold) || threshold <= 0) {
threshold = 1000
}
return threshold > 0 && threshold < 5
}
},
template: ``
})
// 防盗链
Vue.component("http-firewall-checkpoint-referer-block", {
props: ["v-checkpoint"],
data: function () {
let allowEmpty = true
let allowSameDomain = true
let allowDomains = []
let options = {}
if (window.parent.UPDATING_RULE != null) {
options = window.parent.UPDATING_RULE.checkpointOptions
}
if (options == null) {
options = {}
}
if (typeof (options.allowEmpty) == "boolean") {
allowEmpty = options.allowEmpty
}
if (typeof (options.allowSameDomain) == "boolean") {
allowSameDomain = options.allowSameDomain
}
if (options.allowDomains != null && typeof (options.allowDomains) == "object") {
allowDomains = options.allowDomains
}
let that = this
setTimeout(function () {
that.change()
}, 100)
return {
allowEmpty: allowEmpty,
allowSameDomain: allowSameDomain,
allowDomains: allowDomains,
options: {},
value: 0
}
},
watch: {
allowEmpty: function () {
this.change()
},
allowSameDomain: function () {
this.change()
}
},
methods: {
changeAllowDomains: function (values) {
this.allowDomains = values
this.change()
},
change: function () {
this.vCheckpoint.options = [
{
code: "allowEmpty",
value: this.allowEmpty
},
{
code: "allowSameDomain",
value: this.allowSameDomain,
},
{
code: "allowDomains",
value: this.allowDomains
}
]
}
},
template: `
来源域名允许为空
来源域名允许一致
允许的来源域名
`
})
Vue.component("http-access-log-partitions-box", {
props: ["v-partition", "v-day", "v-query"],
mounted: function () {
let that = this
Tea.action("/servers/logs/partitionData")
.params({
day: this.vDay
})
.success(function (resp) {
that.partitions = []
resp.data.partitions.reverse().forEach(function (v) {
that.partitions.push({
code: v,
isDisabled: false,
hasLogs: false
})
})
if (that.partitions.length > 0) {
if (that.vPartition == null || that.vPartition < 0) {
that.selectedPartition = that.partitions[0].code
}
if (that.partitions.length > 1) {
that.checkLogs()
}
}
})
.post()
},
data: function () {
return {
partitions: [],
selectedPartition: this.vPartition,
checkingPartition: 0
}
},
methods: {
url: function (p) {
let u = window.location.toString()
u = u.replace(/\?partition=-?\d+/, "?")
u = u.replace(/\?requestId=-?\d+/, "?")
u = u.replace(/&partition=-?\d+/, "")
u = u.replace(/&requestId=-?\d+/, "")
if (u.indexOf("?") > 0) {
u += "&partition=" + p
} else {
u += "?partition=" + p
}
return u
},
disable: function (partition) {
this.partitions.forEach(function (p) {
if (p.code == partition) {
p.isDisabled = true
}
})
},
checkLogs: function () {
let that = this
let index = this.checkingPartition
let params = {
partition: index
}
let query = this.vQuery
if (query == null || query.length == 0) {
return
}
query.split("&").forEach(function (v) {
let param = v.split("=")
params[param[0]] = decodeURIComponent(param[1])
})
Tea.action("/servers/logs/hasLogs")
.params(params)
.post()
.success(function (response) {
if (response.data.hasLogs) {
// 因为是倒序,所以这里需要使用总长度减去index
that.partitions[that.partitions.length - 1 - index].hasLogs = true
}
index++
if (index >= that.partitions.length) {
return
}
that.checkingPartition = index
that.checkLogs()
})
}
},
template: ``
})
Vue.component("http-cache-refs-config-box", {
props: ["v-cache-refs", "v-cache-config", "v-cache-policy-id", "v-web-id"],
mounted: function () {
let that = this
sortTable(function (ids) {
let newRefs = []
ids.forEach(function (id) {
that.refs.forEach(function (ref) {
if (ref.id == id) {
newRefs.push(ref)
}
})
})
that.updateRefs(newRefs)
that.change()
})
},
data: function () {
let refs = this.vCacheRefs
if (refs == null) {
refs = []
}
let id = 0
refs.forEach(function (ref) {
id++
ref.id = id
})
return {
refs: refs,
id: id // 用来对条件进行排序
}
},
methods: {
addRef: function (isReverse) {
window.UPDATING_CACHE_REF = null
let height = window.innerHeight
if (height > 500) {
height = 500
}
let that = this
teaweb.popup("/servers/server/settings/cache/createPopup?isReverse=" + (isReverse ? 1 : 0), {
height: height + "px",
callback: function (resp) {
let newRef = resp.data.cacheRef
if (newRef.conds == null) {
return
}
that.id++
newRef.id = that.id
if (newRef.isReverse) {
let newRefs = []
let isAdded = false
that.refs.forEach(function (v) {
if (!v.isReverse && !isAdded) {
newRefs.push(newRef)
isAdded = true
}
newRefs.push(v)
})
if (!isAdded) {
newRefs.push(newRef)
}
that.updateRefs(newRefs)
} else {
that.refs.push(newRef)
}
that.change()
}
})
},
updateRef: function (index, cacheRef) {
window.UPDATING_CACHE_REF = cacheRef
let height = window.innerHeight
if (height > 500) {
height = 500
}
let that = this
teaweb.popup("/servers/server/settings/cache/createPopup", {
height: height + "px",
callback: function (resp) {
resp.data.cacheRef.id = that.refs[index].id
Vue.set(that.refs, index, resp.data.cacheRef)
that.change()
that.$refs.cacheRef[index].updateConds(resp.data.cacheRef.conds, resp.data.cacheRef.simpleCond)
that.$refs.cacheRef[index].notifyChange()
}
})
},
disableRef: function (ref) {
ref.isOn = false
this.change()
},
enableRef: function (ref) {
ref.isOn = true
this.change()
},
removeRef: function (index) {
let that = this
teaweb.confirm("确定要删除此缓存设置吗?", function () {
that.refs.$remove(index)
that.change()
})
},
updateRefs: function (newRefs) {
this.refs = newRefs
if (this.vCacheConfig != null) {
this.vCacheConfig.cacheRefs = newRefs
}
},
timeUnitName: function (unit) {
switch (unit) {
case "ms":
return "毫秒"
case "second":
return "秒"
case "minute":
return "分钟"
case "hour":
return "小时"
case "day":
return "天"
case "week":
return "周 "
}
return unit
},
change: function () {
this.$forceUpdate()
// 自动保存
if (this.vCachePolicyId != null && this.vCachePolicyId > 0) { // 缓存策略
Tea.action("/servers/components/cache/updateRefs")
.params({
cachePolicyId: this.vCachePolicyId,
refsJSON: JSON.stringify(this.refs)
})
.post()
} else if (this.vWebId != null && this.vWebId > 0) { // Server Web or Group Web
Tea.action("/servers/server/settings/cache/updateRefs")
.params({
webId: this.vWebId,
refsJSON: JSON.stringify(this.refs)
})
.success(function (resp) {
if (resp.data.isUpdated) {
teaweb.successToast("保存成功")
}
})
.post()
}
}
},
template: `
缓存条件
分组关系
缓存时间
操作
{{cacheRef.minSize.count}}{{cacheRef.minSize.unit}}
- {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}}
0 - {{cacheRef.maxSize.count}}{{cacheRef.maxSize.unit}}
{{cacheRef.methods.join(", ")}}
Expires
状态码:{{cacheRef.status.map(function(v) {return v.toString()}).join(", ")}}
区间缓存
If-None-Match
If-Modified-Since
和
或
{{cacheRef.life.count}} {{timeUnitName(cacheRef.life.unit)}}
不缓存
修改
暂停 恢复
删除
+添加缓存条件 +添加不缓存条件
`
})
Vue.component("origin-list-box", {
props: ["v-primary-origins", "v-backup-origins", "v-server-type", "v-params"],
data: function () {
return {
primaryOrigins: this.vPrimaryOrigins,
backupOrigins: this.vBackupOrigins
}
},
methods: {
createPrimaryOrigin: function () {
teaweb.popup("/servers/server/settings/origins/addPopup?originType=primary&" + this.vParams, {
height: "27em",
callback: function (resp) {
teaweb.success("保存成功", function () {
window.location.reload()
})
}
})
},
createBackupOrigin: function () {
teaweb.popup("/servers/server/settings/origins/addPopup?originType=backup&" + this.vParams, {
height: "27em",
callback: function (resp) {
teaweb.success("保存成功", function () {
window.location.reload()
})
}
})
},
updateOrigin: function (originId, originType) {
teaweb.popup("/servers/server/settings/origins/updatePopup?originType=" + originType + "&" + this.vParams + "&originId=" + originId, {
height: "27em",
callback: function (resp) {
teaweb.success("保存成功", function () {
window.location.reload()
})
}
})
},
deleteOrigin: function (originId, originType) {
let that = this
teaweb.confirm("确定要删除此源站吗?", function () {
Tea.action("/servers/server/settings/origins/delete?" + that.vParams + "&originId=" + originId + "&originType=" + originType)
.post()
.success(function () {
teaweb.success("删除成功", function () {
window.location.reload()
})
})
})
}
},
template: ``
})
Vue.component("origin-list-table", {
props: ["v-origins", "v-origin-type"],
data: function () {
return {}
},
methods: {
deleteOrigin: function (originId) {
this.$emit("deleteOrigin", originId, this.vOriginType)
},
updateOrigin: function (originId) {
this.$emit("updateOrigin", originId, this.vOriginType)
}
},
template: `
源站地址
权重
状态
操作
{{origin.addr}}
{{origin.name}}
证书
主机名: {{origin.host}}
端口跟随
匹配: {{domain}}
{{origin.weight}}
修改
删除
`
})
Vue.component("http-firewall-policy-selector", {
props: ["v-http-firewall-policy"],
mounted: function () {
let that = this
Tea.action("/servers/components/waf/count")
.post()
.success(function (resp) {
that.count = resp.data.count
})
},
data: function () {
let firewallPolicy = this.vHttpFirewallPolicy
return {
count: 0,
firewallPolicy: firewallPolicy
}
},
methods: {
remove: function () {
this.firewallPolicy = null
},
select: function () {
let that = this
teaweb.popup("/servers/components/waf/selectPopup", {
callback: function (resp) {
that.firewallPolicy = resp.data.firewallPolicy
}
})
},
create: function () {
let that = this
teaweb.popup("/servers/components/waf/createPopup", {
height: "26em",
callback: function (resp) {
that.firewallPolicy = resp.data.firewallPolicy
}
})
}
},
template: ``
})
Vue.component("http-websocket-box", {
props: ["v-websocket-ref", "v-websocket-config", "v-is-location", "v-is-group"],
data: function () {
let websocketRef = this.vWebsocketRef
if (websocketRef == null) {
websocketRef = {
isPrior: false,
isOn: false,
websocketId: 0
}
}
let websocketConfig = this.vWebsocketConfig
if (websocketConfig == null) {
websocketConfig = {
id: 0,
isOn: false,
handshakeTimeout: {
count: 30,
unit: "second"
},
allowAllOrigins: true,
allowedOrigins: [],
requestSameOrigin: true,
requestOrigin: ""
}
} else {
if (websocketConfig.handshakeTimeout == null) {
websocketConfig.handshakeTimeout = {
count: 30,
unit: "second",
}
}
if (websocketConfig.allowedOrigins == null) {
websocketConfig.allowedOrigins = []
}
}
return {
websocketRef: websocketRef,
websocketConfig: websocketConfig,
handshakeTimeoutCountString: websocketConfig.handshakeTimeout.count.toString(),
advancedVisible: false
}
},
watch: {
handshakeTimeoutCountString: function (v) {
let count = parseInt(v)
if (!isNaN(count) && count >= 0) {
this.websocketConfig.handshakeTimeout.count = count
} else {
this.websocketConfig.handshakeTimeout.count = 0
}
}
},
methods: {
isOn: function () {
return ((!this.vIsLocation && !this.vIsGroup) || this.websocketRef.isPrior) && this.websocketRef.isOn
},
changeAdvancedVisible: function (v) {
this.advancedVisible = v
},
createOrigin: function () {
let that = this
teaweb.popup("/servers/server/settings/websocket/createOrigin", {
height: "12.5em",
callback: function (resp) {
that.websocketConfig.allowedOrigins.push(resp.data.origin)
}
})
},
removeOrigin: function (index) {
this.websocketConfig.allowedOrigins.$remove(index)
}
},
template: ``
})
Vue.component("http-rewrite-rule-list", {
props: ["v-web-id", "v-rewrite-rules"],
mounted: function () {
setTimeout(this.sort, 1000)
},
data: function () {
let rewriteRules = this.vRewriteRules
if (rewriteRules == null) {
rewriteRules = []
}
return {
rewriteRules: rewriteRules
}
},
methods: {
updateRewriteRule: function (rewriteRuleId) {
teaweb.popup("/servers/server/settings/rewrite/updatePopup?webId=" + this.vWebId + "&rewriteRuleId=" + rewriteRuleId, {
height: "26em",
callback: function () {
window.location.reload()
}
})
},
deleteRewriteRule: function (rewriteRuleId) {
let that = this
teaweb.confirm("确定要删除此重写规则吗?", function () {
Tea.action("/servers/server/settings/rewrite/delete")
.params({
webId: that.vWebId,
rewriteRuleId: rewriteRuleId
})
.post()
.refresh()
})
},
// 排序
sort: function () {
if (this.rewriteRules.length == 0) {
return
}
let that = this
sortTable(function (rowIds) {
Tea.action("/servers/server/settings/rewrite/sort")
.post()
.params({
webId: that.vWebId,
rewriteRuleIds: rowIds
})
.success(function () {
teaweb.success("保存成功")
})
})
}
},
template: `
匹配规则
转发目标
转发方式
状态
操作
{{rule.pattern}}
BREAK
{{rule.redirectStatus}}
Host: {{rule.proxyHost}}
{{rule.replace}}
隐式
显示
修改
删除
`
})
Vue.component("http-rewrite-labels-label", {
props: ["v-class"],
template: ` `
})
Vue.component("server-name-box", {
props: ["v-server-names"],
data: function () {
let serverNames = this.vServerNames;
if (serverNames == null) {
serverNames = []
}
return {
serverNames: serverNames,
isSearching: false,
keyword: ""
}
},
methods: {
addServerName: function () {
window.UPDATING_SERVER_NAME = null
let that = this
teaweb.popup("/servers/addServerNamePopup", {
callback: function (resp) {
var serverName = resp.data.serverName
that.serverNames.push(serverName)
}
});
},
removeServerName: function (index) {
this.serverNames.$remove(index)
},
updateServerName: function (index, serverName) {
window.UPDATING_SERVER_NAME = serverName
let that = this
teaweb.popup("/servers/addServerNamePopup", {
callback: function (resp) {
var serverName = resp.data.serverName
Vue.set(that.serverNames, index, serverName)
}
});
},
showSearchBox: function () {
this.isSearching = !this.isSearching
if (this.isSearching) {
let that = this
setTimeout(function () {
that.$refs.keywordRef.focus()
}, 200)
} else {
this.keyword = ""
}
},
},
watch: {
keyword: function (v) {
this.serverNames.forEach(function (serverName) {
if (v.length == 0) {
serverName.isShowing = true
return
}
if (serverName.subNames == null || serverName.subNames.length == 0) {
if (!teaweb.match(serverName.name, v)) {
serverName.isShowing = false
}
} else {
let found = false
serverName.subNames.forEach(function (subName) {
if (teaweb.match(subName, v)) {
found = true
}
})
serverName.isShowing = found
}
})
}
},
template: `
{{serverName.type}}
{{serverName.name}}
{{serverName.subNames[0]}}等{{serverName.subNames.length}}个域名
`
})
Vue.component("http-cache-stale-config", {
props: ["v-cache-stale-config"],
data: function () {
let config = this.vCacheStaleConfig
if (config == null) {
config = {
isPrior: false,
isOn: false,
status: [],
supportStaleIfErrorHeader: true,
life: {
count: 1,
unit: "day"
}
}
}
return {
config: config
}
},
watch: {
config: {
deep: true,
handler: function () {
this.$emit("change", this.config)
}
}
},
methods: {},
template: `
启用过时缓存
有效期
状态码
支持stale-if-error
`
})
Vue.component("firewall-syn-flood-config-viewer", {
props: ["v-syn-flood-config"],
data: function () {
let config = this.vSynFloodConfig
if (config == null) {
config = {
isOn: false,
minAttempts: 10,
timeoutSeconds: 600,
ignoreLocal: true
}
}
return {
config: config
}
},
template: `
已启用 / 空连接次数:{{config.minAttempts}}次/分钟 / 封禁时间:{{config.timeoutSeconds}}秒 / 忽略局域网访问
未启用
`
})
// 域名列表
Vue.component("domains-box", {
props: ["v-domains"],
data: function () {
let domains = this.vDomains
if (domains == null) {
domains = []
}
return {
domains: domains,
isAdding: false,
addingDomain: ""
}
},
methods: {
add: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.addingDomain.focus()
}, 100)
},
confirm: function () {
let that = this
// 删除其中的空格
this.addingDomain = this.addingDomain.replace(/\s/g, "")
if (this.addingDomain.length == 0) {
teaweb.warn("请输入要添加的域名", function () {
that.$refs.addingDomain.focus()
})
return
}
// 基本校验
if (this.addingDomain[0] == "~") {
let expr = this.addingDomain.substring(1)
try {
new RegExp(expr)
} catch (e) {
teaweb.warn("正则表达式错误:" + e.message, function () {
that.$refs.addingDomain.focus()
})
return
}
}
this.domains.push(this.addingDomain)
this.cancel()
},
remove: function (index) {
this.domains.$remove(index)
},
cancel: function () {
this.isAdding = false
this.addingDomain = ""
}
},
template: `
[正则]
[后缀]
[泛域名]
{{domain}}
+
`
})
Vue.component("http-redirect-to-https-box", {
props: ["v-redirect-to-https-config", "v-is-location"],
data: function () {
let redirectToHttpsConfig = this.vRedirectToHttpsConfig
if (redirectToHttpsConfig == null) {
redirectToHttpsConfig = {
isPrior: false,
isOn: false,
host: "",
port: 0,
status: 0,
onlyDomains: [],
exceptDomains: []
}
} else {
if (redirectToHttpsConfig.onlyDomains == null) {
redirectToHttpsConfig.onlyDomains = []
}
if (redirectToHttpsConfig.exceptDomains == null) {
redirectToHttpsConfig.exceptDomains = []
}
}
return {
redirectToHttpsConfig: redirectToHttpsConfig,
portString: (redirectToHttpsConfig.port > 0) ? redirectToHttpsConfig.port.toString() : "",
moreOptionsVisible: false,
statusOptions: [
{"code": 301, "text": "Moved Permanently"},
{"code": 308, "text": "Permanent Redirect"},
{"code": 302, "text": "Found"},
{"code": 303, "text": "See Other"},
{"code": 307, "text": "Temporary Redirect"}
]
}
},
watch: {
"redirectToHttpsConfig.status": function () {
this.redirectToHttpsConfig.status = parseInt(this.redirectToHttpsConfig.status)
},
portString: function (v) {
let port = parseInt(v)
if (!isNaN(port)) {
this.redirectToHttpsConfig.port = port
} else {
this.redirectToHttpsConfig.port = 0
}
}
},
methods: {
changeMoreOptions: function (isVisible) {
this.moreOptionsVisible = isVisible
},
changeOnlyDomains: function (values) {
this.redirectToHttpsConfig.onlyDomains = values
this.$forceUpdate()
},
changeExceptDomains: function (values) {
this.redirectToHttpsConfig.exceptDomains = values
this.$forceUpdate()
}
},
template: ``
})
// 动作选择
Vue.component("http-firewall-actions-box", {
props: ["v-actions", "v-firewall-policy", "v-action-configs"],
mounted: function () {
let that = this
Tea.action("/servers/iplists/levelOptions")
.success(function (resp) {
that.ipListLevels = resp.data.levels
})
.post()
this.loadJS(function () {
let box = document.getElementById("actions-box")
Sortable.create(box, {
draggable: ".label",
handle: ".icon.handle",
onStart: function () {
that.cancel()
},
onUpdate: function (event) {
let labels = box.getElementsByClassName("label")
let newConfigs = []
for (let i = 0; i < labels.length; i++) {
let index = parseInt(labels[i].getAttribute("data-index"))
newConfigs.push(that.configs[index])
}
that.configs = newConfigs
}
})
})
},
data: function () {
if (this.vFirewallPolicy.inbound == null) {
this.vFirewallPolicy.inbound = {}
}
if (this.vFirewallPolicy.inbound.groups == null) {
this.vFirewallPolicy.inbound.groups = []
}
let id = 0
let configs = []
if (this.vActionConfigs != null) {
configs = this.vActionConfigs
configs.forEach(function (v) {
v.id = (id++)
})
}
var defaultPageBody = `
403 Forbidden
403 Forbidden
Request ID: \${requestId}.
`
return {
id: id,
actions: this.vActions,
configs: configs,
isAdding: false,
editingIndex: -1,
action: null,
actionCode: "",
actionOptions: {},
// IPList相关
ipListLevels: [],
// 动作参数
blockTimeout: "",
blockScope: "global",
captchaLife: "",
captchaMaxFails: "",
captchaFailBlockTimeout: "",
get302Life: "",
post307Life: "",
recordIPType: "black",
recordIPLevel: "critical",
recordIPTimeout: "",
recordIPListId: 0,
recordIPListName: "",
tagTags: [],
pageStatus: 403,
pageBody: defaultPageBody,
defaultPageBody: defaultPageBody,
goGroupName: "",
goGroupId: 0,
goGroup: null,
goSetId: 0,
goSetName: "",
jsCookieLife: "",
jsCookieMaxFails: "",
jsCookieFailBlockTimeout: ""
}
},
watch: {
actionCode: function (code) {
this.action = this.actions.$find(function (k, v) {
return v.code == code
})
this.actionOptions = {}
},
blockTimeout: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["timeout"] = 0
} else {
this.actionOptions["timeout"] = v
}
},
blockScope: function (v) {
this.actionOptions["scope"] = v
},
captchaLife: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["life"] = 0
} else {
this.actionOptions["life"] = v
}
},
captchaMaxFails: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["maxFails"] = 0
} else {
this.actionOptions["maxFails"] = v
}
},
captchaFailBlockTimeout: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["failBlockTimeout"] = 0
} else {
this.actionOptions["failBlockTimeout"] = v
}
},
get302Life: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["life"] = 0
} else {
this.actionOptions["life"] = v
}
},
post307Life: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["life"] = 0
} else {
this.actionOptions["life"] = v
}
},
recordIPType: function (v) {
this.recordIPListId = 0
},
recordIPTimeout: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["timeout"] = 0
} else {
this.actionOptions["timeout"] = v
}
},
goGroupId: function (groupId) {
let group = this.vFirewallPolicy.inbound.groups.$find(function (k, v) {
return v.id == groupId
})
this.goGroup = group
if (group == null) {
this.goGroupName = ""
} else {
this.goGroupName = group.name
}
this.goSetId = 0
this.goSetName = ""
},
goSetId: function (setId) {
if (this.goGroup == null) {
return
}
let set = this.goGroup.sets.$find(function (k, v) {
return v.id == setId
})
if (set == null) {
this.goSetId = 0
this.goSetName = ""
} else {
this.goSetName = set.name
}
},
jsCookieLife: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["life"] = 0
} else {
this.actionOptions["life"] = v
}
},
jsCookieMaxFails: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["maxFails"] = 0
} else {
this.actionOptions["maxFails"] = v
}
},
jsCookieFailBlockTimeout: function (v) {
v = parseInt(v)
if (isNaN(v)) {
this.actionOptions["failBlockTimeout"] = 0
} else {
this.actionOptions["failBlockTimeout"] = v
}
},
},
methods: {
add: function () {
this.action = null
this.actionCode = "block"
this.isAdding = true
this.actionOptions = {}
// 动作参数
this.blockTimeout = ""
this.blockScope = "global"
this.captchaLife = ""
this.captchaMaxFails = ""
this.captchaFailBlockTimeout = ""
this.jsCookieLife = ""
this.jsCookieMaxFails = ""
this.jsCookieFailBlockTimeout = ""
this.get302Life = ""
this.post307Life = ""
this.recordIPLevel = "critical"
this.recordIPType = "black"
this.recordIPTimeout = ""
this.recordIPListId = 0
this.recordIPListName = ""
this.tagTags = []
this.pageStatus = 403
this.pageBody = this.defaultPageBody
this.goGroupName = ""
this.goGroupId = 0
this.goGroup = null
this.goSetId = 0
this.goSetName = ""
let that = this
this.action = this.vActions.$find(function (k, v) {
return v.code == that.actionCode
})
// 滚到界面底部
this.scroll()
},
remove: function (index) {
this.isAdding = false
this.editingIndex = -1
this.configs.$remove(index)
},
update: function (index, config) {
if (this.isAdding && this.editingIndex == index) {
this.cancel()
return
}
this.add()
this.isAdding = true
this.editingIndex = index
this.actionCode = config.code
switch (config.code) {
case "block":
this.blockTimeout = ""
if (config.options.timeout != null || config.options.timeout > 0) {
this.blockTimeout = config.options.timeout.toString()
}
if (config.options.scope != null && config.options.scope.length > 0) {
this.blockScope = config.options.scope
} else {
this.blockScope = "global" // 兼容先前版本遗留的默认值
}
break
case "allow":
break
case "log":
break
case "captcha":
this.captchaLife = ""
if (config.options.life != null || config.options.life > 0) {
this.captchaLife = config.options.life.toString()
}
this.captchaMaxFails = ""
if (config.options.maxFails != null || config.options.maxFails > 0) {
this.captchaMaxFails = config.options.maxFails.toString()
}
this.captchaFailBlockTimeout = ""
if (config.options.failBlockTimeout != null || config.options.failBlockTimeout > 0) {
this.captchaFailBlockTimeout = config.options.failBlockTimeout.toString()
}
break
case "js_cookie":
this.jsCookieLife = ""
if (config.options.life != null || config.options.life > 0) {
this.jsCookieLife = config.options.life.toString()
}
this.jsCookieMaxFails = ""
if (config.options.maxFails != null || config.options.maxFails > 0) {
this.jsCookieMaxFails = config.options.maxFails.toString()
}
this.jsCookieFailBlockTimeout = ""
if (config.options.failBlockTimeout != null || config.options.failBlockTimeout > 0) {
this.jsCookieFailBlockTimeout = config.options.failBlockTimeout.toString()
}
break
case "notify":
break
case "get_302":
this.get302Life = ""
if (config.options.life != null || config.options.life > 0) {
this.get302Life = config.options.life.toString()
}
break
case "post_307":
this.post307Life = ""
if (config.options.life != null || config.options.life > 0) {
this.post307Life = config.options.life.toString()
}
break;
case "record_ip":
if (config.options != null) {
this.recordIPLevel = config.options.level
this.recordIPType = config.options.type
if (config.options.timeout > 0) {
this.recordIPTimeout = config.options.timeout.toString()
}
let that = this
// VUE需要在函数执行完之后才会调用watch函数,这样会导致设置的值被覆盖,所以这里使用setTimeout
setTimeout(function () {
that.recordIPListId = config.options.ipListId
that.recordIPListName = config.options.ipListName
})
}
break
case "tag":
this.tagTags = []
if (config.options.tags != null) {
this.tagTags = config.options.tags
}
break
case "page":
this.pageStatus = 403
this.pageBody = this.defaultPageBody
if (config.options.status != null) {
this.pageStatus = config.options.status
}
if (config.options.body != null) {
this.pageBody = config.options.body
}
break
case "go_group":
if (config.options != null) {
this.goGroupName = config.options.groupName
this.goGroupId = config.options.groupId
this.goGroup = this.vFirewallPolicy.inbound.groups.$find(function (k, v) {
return v.id == config.options.groupId
})
}
break
case "go_set":
if (config.options != null) {
this.goGroupName = config.options.groupName
this.goGroupId = config.options.groupId
this.goGroup = this.vFirewallPolicy.inbound.groups.$find(function (k, v) {
return v.id == config.options.groupId
})
// VUE需要在函数执行完之后才会调用watch函数,这样会导致设置的值被覆盖,所以这里使用setTimeout
let that = this
setTimeout(function () {
that.goSetId = config.options.setId
if (that.goGroup != null) {
let set = that.goGroup.sets.$find(function (k, v) {
return v.id == config.options.setId
})
if (set != null) {
that.goSetName = set.name
}
}
})
}
break
}
// 滚到界面底部
this.scroll()
},
cancel: function () {
this.isAdding = false
this.editingIndex = -1
},
confirm: function () {
if (this.action == null) {
return
}
if (this.actionOptions == null) {
this.actionOptions = {}
}
// record_ip
if (this.actionCode == "record_ip") {
let timeout = parseInt(this.recordIPTimeout)
if (isNaN(timeout)) {
timeout = 0
}
if (this.recordIPListId <= 0) {
return
}
this.actionOptions = {
type: this.recordIPType,
level: this.recordIPLevel,
timeout: timeout,
ipListId: this.recordIPListId,
ipListName: this.recordIPListName
}
} else if (this.actionCode == "tag") { // tag
if (this.tagTags == null || this.tagTags.length == 0) {
return
}
this.actionOptions = {
tags: this.tagTags
}
} else if (this.actionCode == "page") {
let pageStatus = this.pageStatus.toString()
if (!pageStatus.match(/^\d{3}$/)) {
pageStatus = 403
} else {
pageStatus = parseInt(pageStatus)
}
this.actionOptions = {
status: pageStatus,
body: this.pageBody
}
} else if (this.actionCode == "go_group") { // go_group
let groupId = this.goGroupId
if (typeof (groupId) == "string") {
groupId = parseInt(groupId)
if (isNaN(groupId)) {
groupId = 0
}
}
if (groupId <= 0) {
return
}
this.actionOptions = {
groupId: groupId.toString(),
groupName: this.goGroupName
}
} else if (this.actionCode == "go_set") { // go_set
let groupId = this.goGroupId
if (typeof (groupId) == "string") {
groupId = parseInt(groupId)
if (isNaN(groupId)) {
groupId = 0
}
}
let setId = this.goSetId
if (typeof (setId) == "string") {
setId = parseInt(setId)
if (isNaN(setId)) {
setId = 0
}
}
if (setId <= 0) {
return
}
this.actionOptions = {
groupId: groupId.toString(),
groupName: this.goGroupName,
setId: setId.toString(),
setName: this.goSetName
}
}
let options = {}
for (let k in this.actionOptions) {
if (this.actionOptions.hasOwnProperty(k)) {
options[k] = this.actionOptions[k]
}
}
if (this.editingIndex > -1) {
this.configs[this.editingIndex] = {
id: this.configs[this.editingIndex].id,
code: this.actionCode,
name: this.action.name,
options: options
}
} else {
this.configs.push({
id: (this.id++),
code: this.actionCode,
name: this.action.name,
options: options
})
}
this.cancel()
},
removeRecordIPList: function () {
this.recordIPListId = 0
},
selectRecordIPList: function () {
let that = this
teaweb.popup("/servers/iplists/selectPopup?type=" + this.recordIPType, {
width: "50em",
height: "30em",
callback: function (resp) {
that.recordIPListId = resp.data.list.id
that.recordIPListName = resp.data.list.name
}
})
},
changeTags: function (tags) {
this.tagTags = tags
},
loadJS: function (callback) {
if (typeof Sortable != "undefined") {
callback()
return
}
// 引入js
let jsFile = document.createElement("script")
jsFile.setAttribute("src", "/js/sortable.min.js")
jsFile.addEventListener("load", function () {
callback()
})
document.head.appendChild(jsFile)
},
scroll: function () {
setTimeout(function () {
let mainDiv = document.getElementsByClassName("main")
if (mainDiv.length > 0) {
mainDiv[0].scrollTo(0, 1000)
}
}, 10)
}
},
template: `
{{config.name}}
({{config.code.toUpperCase()}})
:有效期{{config.options.timeout}}秒
:有效期{{config.options.life}}秒
/ 最多失败{{config.options.maxFails}}次
:有效期{{config.options.life}}秒
/ 最多失败{{config.options.maxFails}}次
:有效期{{config.options.life}}秒
:有效期{{config.options.life}}秒
:{{config.options.ipListName}}
:{{config.options.tags.join(", ")}}
:[{{config.options.status}}]
:{{config.options.groupName}}
:{{config.options.groupName}} / {{config.options.setName}}
[所有服务]
[当前服务]
+
`
})
// 认证设置
Vue.component("http-auth-config-box", {
props: ["v-auth-config", "v-is-location"],
data: function () {
let authConfig = this.vAuthConfig
if (authConfig == null) {
authConfig = {
isPrior: false,
isOn: false
}
}
if (authConfig.policyRefs == null) {
authConfig.policyRefs = []
}
return {
authConfig: authConfig
}
},
methods: {
isOn: function () {
return (!this.vIsLocation || this.authConfig.isPrior) && this.authConfig.isOn
},
add: function () {
let that = this
teaweb.popup("/servers/server/settings/access/createPopup", {
callback: function (resp) {
that.authConfig.policyRefs.push(resp.data.policyRef)
that.change()
},
height: "28em"
})
},
update: function (index, policyId) {
let that = this
teaweb.popup("/servers/server/settings/access/updatePopup?policyId=" + policyId, {
callback: function (resp) {
teaweb.success("保存成功", function () {
teaweb.reload()
})
},
height: "28em"
})
},
remove: function (index) {
this.authConfig.policyRefs.$remove(index)
this.change()
},
methodName: function (methodType) {
switch (methodType) {
case "basicAuth":
return "BasicAuth"
case "subRequest":
return "子请求"
case "typeA":
return "URL鉴权A"
case "typeB":
return "URL鉴权B"
case "typeC":
return "URL鉴权C"
case "typeD":
return "URL鉴权D"
}
return ""
},
change: function () {
let that = this
setTimeout(function () {
// 延时通知,是为了让表单有机会变更数据
that.$emit("change", this.authConfig)
}, 100)
}
},
template: `
鉴权方式
名称
鉴权方法
参数
状态
操作
{{ref.authPolicy.name}}
{{methodName(ref.authPolicy.type)}}
{{ref.authPolicy.params.users.length}}个用户
[{{ref.authPolicy.params.method}}]
{{ref.authPolicy.params.url}}
{{ref.authPolicy.params.signParamName}}/有效期{{ref.authPolicy.params.life}}秒
有效期{{ref.authPolicy.params.life}}秒
有效期{{ref.authPolicy.params.life}}秒
{{ref.authPolicy.params.signParamName}}/{{ref.authPolicy.params.timestampParamName}}/有效期{{ref.authPolicy.params.life}}秒
扩展名:{{ext}}
域名:{{domain}}
修改
删除
+添加鉴权方式
`
})
Vue.component("user-selector", {
props: ["v-user-id", "data-url"],
data: function () {
let userId = this.vUserId
if (userId == null) {
userId = 0
}
let dataURL = this.dataUrl
if (dataURL == null || dataURL.length == 0) {
dataURL = "/servers/users/options"
}
return {
users: [],
userId: userId,
dataURL: dataURL
}
},
methods: {
change: function(item) {
if (item != null) {
this.$emit("change", item.id)
} else {
this.$emit("change", 0)
}
}
},
template: `
`
})
// UAM模式配置
Vue.component("uam-config-box", {
props: ["v-uam-config", "v-is-location", "v-is-group"],
data: function () {
let config = this.vUamConfig
if (config == null) {
config = {
isPrior: false,
isOn: false
}
}
return {
config: config
}
},
template: ``
})
Vue.component("http-header-policy-box", {
props: ["v-request-header-policy", "v-request-header-ref", "v-response-header-policy", "v-response-header-ref", "v-params", "v-is-location", "v-is-group", "v-has-group-request-config", "v-has-group-response-config", "v-group-setting-url"],
data: function () {
let type = "response"
let hash = window.location.hash
if (hash == "#request") {
type = "request"
}
// ref
let requestHeaderRef = this.vRequestHeaderRef
if (requestHeaderRef == null) {
requestHeaderRef = {
isPrior: false,
isOn: true,
headerPolicyId: 0
}
}
let responseHeaderRef = this.vResponseHeaderRef
if (responseHeaderRef == null) {
responseHeaderRef = {
isPrior: false,
isOn: true,
headerPolicyId: 0
}
}
// 请求相关
let requestSettingHeaders = []
let requestDeletingHeaders = []
let requestPolicy = this.vRequestHeaderPolicy
if (requestPolicy != null) {
if (requestPolicy.setHeaders != null) {
requestSettingHeaders = requestPolicy.setHeaders
}
if (requestPolicy.deleteHeaders != null) {
requestDeletingHeaders = requestPolicy.deleteHeaders
}
}
// 响应相关
let responseSettingHeaders = []
let responseDeletingHeaders = []
let responsePolicy = this.vResponseHeaderPolicy
if (responsePolicy != null) {
if (responsePolicy.setHeaders != null) {
responseSettingHeaders = responsePolicy.setHeaders
}
if (responsePolicy.deleteHeaders != null) {
responseDeletingHeaders = responsePolicy.deleteHeaders
}
}
return {
type: type,
typeName: (type == "request") ? "请求" : "响应",
requestHeaderRef: requestHeaderRef,
responseHeaderRef: responseHeaderRef,
requestSettingHeaders: requestSettingHeaders,
requestDeletingHeaders: requestDeletingHeaders,
responseSettingHeaders: responseSettingHeaders,
responseDeletingHeaders: responseDeletingHeaders
}
},
methods: {
selectType: function (type) {
this.type = type
window.location.hash = "#" + type
window.location.reload()
},
addSettingHeader: function (policyId) {
teaweb.popup("/servers/server/settings/headers/createSetPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + this.type, {
callback: function () {
teaweb.successRefresh("保存成功")
}
})
},
addDeletingHeader: function (policyId, type) {
teaweb.popup("/servers/server/settings/headers/createDeletePopup?" + this.vParams + "&headerPolicyId=" + policyId + "&type=" + type, {
callback: function () {
teaweb.successRefresh("保存成功")
}
})
},
updateSettingPopup: function (policyId, headerId) {
teaweb.popup("/servers/server/settings/headers/updateSetPopup?" + this.vParams + "&headerPolicyId=" + policyId + "&headerId=" + headerId+ "&type=" + this.type, {
callback: function () {
teaweb.successRefresh("保存成功")
}
})
},
deleteDeletingHeader: function (policyId, headerName) {
teaweb.confirm("确定要删除'" + headerName + "'吗?", function () {
Tea.action("/servers/server/settings/headers/deleteDeletingHeader")
.params({
headerPolicyId: policyId,
headerName: headerName
})
.post()
.refresh()
})
},
deleteHeader: function (policyId, type, headerId) {
teaweb.confirm("确定要删除此Header吗?", function () {
this.$post("/servers/server/settings/headers/delete")
.params({
headerPolicyId: policyId,
type: type,
headerId: headerId
})
.refresh()
}
)
}
},
template: `
由于已经在当前服务分组 中进行了对应的配置,在这里的配置将不会生效。
名称
值
操作
{{header.name}}
{{code}}
{{method}}
{{domain}}
附加
跳转禁用
替换
{{header.value}}
修改 删除
删除请求Header
由于已经在当前服务分组 中进行了对应的配置,在这里的配置将不会生效。
名称
值
操作
{{header.name}}
{{code}}
{{method}}
{{domain}}
附加
跳转禁用
替换
{{header.value}}
修改 删除
删除响应Header
`
})
// 通用设置
Vue.component("http-common-config-box", {
props: ["v-common-config"],
data: function () {
let config = this.vCommonConfig
if (config == null) {
config = {
mergeSlashes: false
}
}
return {
config: config
}
},
template: ``
})
Vue.component("http-cache-policy-selector", {
props: ["v-cache-policy"],
mounted: function () {
let that = this
Tea.action("/servers/components/cache/count")
.post()
.success(function (resp) {
that.count = resp.data.count
})
},
data: function () {
let cachePolicy = this.vCachePolicy
return {
count: 0,
cachePolicy: cachePolicy
}
},
methods: {
remove: function () {
this.cachePolicy = null
},
select: function () {
let that = this
teaweb.popup("/servers/components/cache/selectPopup", {
callback: function (resp) {
that.cachePolicy = resp.data.cachePolicy
}
})
},
create: function () {
let that = this
teaweb.popup("/servers/components/cache/createPopup", {
height: "26em",
callback: function (resp) {
that.cachePolicy = resp.data.cachePolicy
}
})
}
},
template: ``
})
Vue.component("http-pages-and-shutdown-box", {
props: ["v-pages", "v-shutdown-config", "v-is-location"],
data: function () {
let pages = []
if (this.vPages != null) {
pages = this.vPages
}
let shutdownConfig = {
isPrior: false,
isOn: false,
bodyType: "url",
url: "",
body: "",
status: 0
}
if (this.vShutdownConfig != null) {
if (this.vShutdownConfig.body == null) {
this.vShutdownConfig.body = ""
}
if (this.vShutdownConfig.bodyType == null) {
this.vShutdownConfig.bodyType = "url"
}
shutdownConfig = this.vShutdownConfig
}
let shutdownStatus = ""
if (shutdownConfig.status > 0) {
shutdownStatus = shutdownConfig.status.toString()
}
return {
pages: pages,
shutdownConfig: shutdownConfig,
shutdownStatus: shutdownStatus
}
},
watch: {
shutdownStatus: function (status) {
let statusInt = parseInt(status)
if (!isNaN(statusInt) && statusInt > 0 && statusInt < 1000) {
this.shutdownConfig.status = statusInt
} else {
this.shutdownConfig.status = 0
}
}
},
methods: {
addPage: function () {
let that = this
teaweb.popup("/servers/server/settings/pages/createPopup", {
height: "26em",
callback: function (resp) {
that.pages.push(resp.data.page)
}
})
},
updatePage: function (pageIndex, pageId) {
let that = this
teaweb.popup("/servers/server/settings/pages/updatePopup?pageId=" + pageId, {
height: "26em",
callback: function (resp) {
Vue.set(that.pages, pageIndex, resp.data.page)
}
})
},
removePage: function (pageIndex) {
let that = this
teaweb.confirm("确定要移除此页面吗?", function () {
that.pages.$remove(pageIndex)
})
},
addShutdownHTMLTemplate: function () {
this.shutdownConfig.body = `
\t升级中
\t
网站升级中
为了给您提供更好的服务,我们正在升级网站,请稍后重新访问。
Request ID: \${requestId}.
`
}
},
template: `
自定义页面
{{page.status}} ->
{{page.url}} [HTML内容]
+
临时关闭页面
`
})
// 压缩配置
Vue.component("http-compression-config-box", {
props: ["v-compression-config", "v-is-location", "v-is-group"],
mounted: function () {
let that = this
sortLoad(function () {
that.initSortableTypes()
})
},
data: function () {
let config = this.vCompressionConfig
if (config == null) {
config = {
isPrior: false,
isOn: false,
useDefaultTypes: true,
types: ["brotli", "gzip", "zstd", "deflate"],
level: 5,
decompressData: false,
gzipRef: null,
deflateRef: null,
brotliRef: null,
minLength: {count: 0, "unit": "kb"},
maxLength: {count: 0, "unit": "kb"},
mimeTypes: ["text/*", "application/*", "font/*"],
extensions: [".js", ".json", ".html", ".htm", ".xml", ".css", ".woff2", ".txt"],
conds: null
}
}
if (config.types == null) {
config.types = []
}
if (config.mimeTypes == null) {
config.mimeTypes = []
}
if (config.extensions == null) {
config.extensions = []
}
let allTypes = [
{
name: "Gzip",
code: "gzip",
isOn: true
},
{
name: "Deflate",
code: "deflate",
isOn: true
},
{
name: "Brotli",
code: "brotli",
isOn: true
},
{
name: "ZSTD",
code: "zstd",
isOn: true
}
]
let configTypes = []
config.types.forEach(function (typeCode) {
allTypes.forEach(function (t) {
if (typeCode == t.code) {
t.isOn = true
configTypes.push(t)
}
})
})
allTypes.forEach(function (t) {
if (!config.types.$contains(t.code)) {
t.isOn = false
configTypes.push(t)
}
})
return {
config: config,
moreOptionsVisible: false,
allTypes: configTypes
}
},
watch: {
"config.level": function (v) {
let level = parseInt(v)
if (isNaN(level)) {
level = 1
} else if (level < 1) {
level = 1
} else if (level > 10) {
level = 10
}
this.config.level = level
}
},
methods: {
isOn: function () {
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
},
changeExtensions: function (values) {
values.forEach(function (v, k) {
if (v.length > 0 && v[0] != ".") {
values[k] = "." + v
}
})
this.config.extensions = values
},
changeMimeTypes: function (values) {
this.config.mimeTypes = values
},
changeAdvancedVisible: function () {
this.moreOptionsVisible = !this.moreOptionsVisible
},
changeConds: function (conds) {
this.config.conds = conds
},
changeType: function () {
this.config.types = []
let that = this
this.allTypes.forEach(function (v) {
if (v.isOn) {
that.config.types.push(v.code)
}
})
},
initSortableTypes: function () {
let box = document.querySelector("#compression-types-box")
let that = this
Sortable.create(box, {
draggable: ".checkbox",
handle: ".icon.handle",
onStart: function () {
},
onUpdate: function (event) {
let checkboxes = box.querySelectorAll(".checkbox")
let codes = []
checkboxes.forEach(function (checkbox) {
let code = checkbox.getAttribute("data-code")
codes.push(code)
})
that.config.types = codes
}
})
}
},
template: ``
})
Vue.component("firewall-event-level-options", {
props: ["v-value"],
mounted: function () {
let that = this
Tea.action("/ui/eventLevelOptions")
.post()
.success(function (resp) {
that.levels = resp.data.eventLevels
that.change()
})
},
data: function () {
let value = this.vValue
if (value == null || value.length == 0) {
value = "" // 不要给默认值,因为黑白名单等默认值均有不同
}
return {
levels: [],
description: "",
level: value
}
},
methods: {
change: function () {
this.$emit("change")
let that = this
let l = this.levels.$find(function (k, v) {
return v.code == that.level
})
if (l != null) {
this.description = l.description
} else {
this.description = ""
}
}
},
template: `
{{level.name}}
`
})
Vue.component("prior-checkbox", {
props: ["v-config", "description"],
data: function () {
let description = this.description
if (description == null) {
description = "打开后可以覆盖父级或子级配置"
}
return {
isPrior: this.vConfig.isPrior,
realDescription: description
}
},
watch: {
isPrior: function (v) {
this.vConfig.isPrior = v
}
},
template: `
打开独立配置
`
})
Vue.component("http-charsets-box", {
props: ["v-usual-charsets", "v-all-charsets", "v-charset-config", "v-is-location", "v-is-group"],
data: function () {
let charsetConfig = this.vCharsetConfig
if (charsetConfig == null) {
charsetConfig = {
isPrior: false,
isOn: false,
charset: "",
isUpper: false
}
}
return {
charsetConfig: charsetConfig,
advancedVisible: false
}
},
methods: {
changeAdvancedVisible: function (v) {
this.advancedVisible = v
}
},
template: ``
})
Vue.component("http-expires-time-config-box", {
props: ["v-expires-time"],
data: function () {
let expiresTime = this.vExpiresTime
if (expiresTime == null) {
expiresTime = {
isPrior: false,
isOn: false,
overwrite: true,
autoCalculate: true,
duration: {count: -1, "unit": "hour"}
}
}
return {
expiresTime: expiresTime
}
},
watch: {
"expiresTime.isPrior": function () {
this.notifyChange()
},
"expiresTime.isOn": function () {
this.notifyChange()
},
"expiresTime.overwrite": function () {
this.notifyChange()
},
"expiresTime.autoCalculate": function () {
this.notifyChange()
}
},
methods: {
notifyChange: function () {
this.$emit("change", this.expiresTime)
}
},
template: ``
})
Vue.component("http-access-log-box", {
props: ["v-access-log", "v-keyword", "v-show-server-link"],
data: function () {
let accessLog = this.vAccessLog
if (accessLog.header != null && accessLog.header.Upgrade != null && accessLog.header.Upgrade.values != null && accessLog.header.Upgrade.values.$contains("websocket")) {
if (accessLog.scheme == "http") {
accessLog.scheme = "ws"
} else if (accessLog.scheme == "https") {
accessLog.scheme = "wss"
}
}
// 对TAG去重
if (accessLog.tags != null && accessLog.tags.length > 0) {
let tagMap = {}
accessLog.tags = accessLog.tags.$filter(function (k, tag) {
let b = (typeof (tagMap[tag]) == "undefined")
tagMap[tag] = true
return b
})
}
// 域名
accessLog.unicodeHost = ""
if (accessLog.host != null && accessLog.host.startsWith("xn--")) {
// port
let portIndex = accessLog.host.indexOf(":")
if (portIndex > 0) {
accessLog.unicodeHost = punycode.ToUnicode(accessLog.host.substring(0, portIndex))
} else {
accessLog.unicodeHost = punycode.ToUnicode(accessLog.host)
}
}
return {
accessLog: accessLog
}
},
methods: {
formatCost: function (seconds) {
if (seconds == null) {
return "0"
}
let s = (seconds * 1000).toString();
let pieces = s.split(".");
if (pieces.length < 2) {
return s;
}
return pieces[0] + "." + pieces[1].substring(0, 3);
},
showLog: function () {
let that = this
let requestId = this.accessLog.requestId
this.$parent.$children.forEach(function (v) {
if (v.deselect != null) {
v.deselect()
}
})
this.select()
teaweb.popup("/servers/server/log/viewPopup?requestId=" + requestId, {
width: "50em",
height: "28em",
onClose: function () {
that.deselect()
}
})
},
select: function () {
this.$refs.box.parentNode.style.cssText = "background: rgba(0, 0, 0, 0.1)"
},
deselect: function () {
this.$refs.box.parentNode.style.cssText = ""
},
mismatch: function () {
teaweb.warn("当前访问没有匹配到任何网站服务")
}
},
template: `
[{{accessLog.node.name}}节点 ]
[服务]
[服务]
[{{accessLog.region}}]
{{accessLog.remoteAddr}} [{{accessLog.timeLocal}}]
"{{accessLog.requestMethod}} {{accessLog.scheme}}://{{accessLog.host}} {{accessLog.requestURI}} {{accessLog.proto}}" {{accessLog.status}}
{{accessLog.unicodeHost}}
cache {{accessLog.attrs['cache.status'].toLowerCase()}}
waf {{accessLog.firewallActions}}
- {{tag}}
WAF -
{{accessLog.wafInfo.group.name}} -
{{accessLog.wafInfo.set.name}}
- 耗时:{{formatCost(accessLog.requestTime)}} ms ({{accessLog.humanTime}})
`
})
// Javascript Punycode converter derived from example in RFC3492.
// This implementation is created by some@domain.name and released into public domain
// 代码来自:https://stackoverflow.com/questions/183485/converting-punycode-with-dash-character-to-unicode
var punycode = new function Punycode() {
// This object converts to and from puny-code used in IDN
//
// punycode.ToASCII ( domain )
//
// Returns a puny coded representation of "domain".
// It only converts the part of the domain name that
// has non ASCII characters. I.e. it dosent matter if
// you call it with a domain that already is in ASCII.
//
// punycode.ToUnicode (domain)
//
// Converts a puny-coded domain name to unicode.
// It only converts the puny-coded parts of the domain name.
// I.e. it dosent matter if you call it on a string
// that already has been converted to unicode.
//
//
this.utf16 = {
// The utf16-class is necessary to convert from javascripts internal character representation to unicode and back.
decode: function (input) {
var output = [], i = 0, len = input.length, value, extra;
while (i < len) {
value = input.charCodeAt(i++);
if ((value & 0xF800) === 0xD800) {
extra = input.charCodeAt(i++);
if (((value & 0xFC00) !== 0xD800) || ((extra & 0xFC00) !== 0xDC00)) {
throw new RangeError("UTF-16(decode): Illegal UTF-16 sequence");
}
value = ((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000;
}
output.push(value);
}
return output;
},
encode: function (input) {
var output = [], i = 0, len = input.length, value;
while (i < len) {
value = input[i++];
if ((value & 0xF800) === 0xD800) {
throw new RangeError("UTF-16(encode): Illegal UTF-16 value");
}
if (value > 0xFFFF) {
value -= 0x10000;
output.push(String.fromCharCode(((value >>> 10) & 0x3FF) | 0xD800));
value = 0xDC00 | (value & 0x3FF);
}
output.push(String.fromCharCode(value));
}
return output.join("");
}
}
//Default parameters
var initial_n = 0x80;
var initial_bias = 72;
var delimiter = "\x2D";
var base = 36;
var damp = 700;
var tmin = 1;
var tmax = 26;
var skew = 38;
var maxint = 0x7FFFFFFF;
// decode_digit(cp) returns the numeric value of a basic code
// point (for use in representing integers) in the range 0 to
// base-1, or base if cp is does not represent a value.
function decode_digit(cp) {
return cp - 48 < 10 ? cp - 22 : cp - 65 < 26 ? cp - 65 : cp - 97 < 26 ? cp - 97 : base;
}
// encode_digit(d,flag) returns the basic code point whose value
// (when used for representing integers) is d, which needs to be in
// the range 0 to base-1. The lowercase form is used unless flag is
// nonzero, in which case the uppercase form is used. The behavior
// is undefined if flag is nonzero and digit d has no uppercase form.
function encode_digit(d, flag) {
return d + 22 + 75 * (d < 26) - ((flag != 0) << 5);
// 0..25 map to ASCII a..z or A..Z
// 26..35 map to ASCII 0..9
}
//** Bias adaptation function **
function adapt(delta, numpoints, firsttime) {
var k;
delta = firsttime ? Math.floor(delta / damp) : (delta >> 1);
delta += Math.floor(delta / numpoints);
for (k = 0; delta > (((base - tmin) * tmax) >> 1); k += base) {
delta = Math.floor(delta / (base - tmin));
}
return Math.floor(k + (base - tmin + 1) * delta / (delta + skew));
}
// encode_basic(bcp,flag) forces a basic code point to lowercase if flag is zero,
// uppercase if flag is nonzero, and returns the resulting code point.
// The code point is unchanged if it is caseless.
// The behavior is undefined if bcp is not a basic code point.
function encode_basic(bcp, flag) {
bcp -= (bcp - 97 < 26) << 5;
return bcp + ((!flag && (bcp - 65 < 26)) << 5);
}
// Main decode
this.decode = function (input, preserveCase) {
// Dont use utf16
var output = [];
var case_flags = [];
var input_length = input.length;
var n, out, i, bias, basic, j, ic, oldi, w, k, digit, t, len;
// Initialize the state:
n = initial_n;
i = 0;
bias = initial_bias;
// Handle the basic code points: Let basic be the number of input code
// points before the last delimiter, or 0 if there is none, then
// copy the first basic code points to the output.
basic = input.lastIndexOf(delimiter);
if (basic < 0) basic = 0;
for (j = 0; j < basic; ++j) {
if (preserveCase) case_flags[output.length] = (input.charCodeAt(j) - 65 < 26);
if (input.charCodeAt(j) >= 0x80) {
throw new RangeError("Illegal input >= 0x80");
}
output.push(input.charCodeAt(j));
}
// Main decoding loop: Start just after the last delimiter if any
// basic code points were copied; start at the beginning otherwise.
for (ic = basic > 0 ? basic + 1 : 0; ic < input_length;) {
// ic is the index of the next character to be consumed,
// Decode a generalized variable-length integer into delta,
// which gets added to i. The overflow checking is easier
// if we increase i as we go, then subtract off its starting
// value at the end to obtain delta.
for (oldi = i, w = 1, k = base; ; k += base) {
if (ic >= input_length) {
throw RangeError("punycode_bad_input(1)");
}
digit = decode_digit(input.charCodeAt(ic++));
if (digit >= base) {
throw RangeError("punycode_bad_input(2)");
}
if (digit > Math.floor((maxint - i) / w)) {
throw RangeError("punycode_overflow(1)");
}
i += digit * w;
t = k <= bias ? tmin : k >= bias + tmax ? tmax : k - bias;
if (digit < t) {
break;
}
if (w > Math.floor(maxint / (base - t))) {
throw RangeError("punycode_overflow(2)");
}
w *= (base - t);
}
out = output.length + 1;
bias = adapt(i - oldi, out, oldi === 0);
// i was supposed to wrap around from out to 0,
// incrementing n each time, so we'll fix that now:
if (Math.floor(i / out) > maxint - n) {
throw RangeError("punycode_overflow(3)");
}
n += Math.floor(i / out);
i %= out;
// Insert n at position i of the output:
// Case of last character determines uppercase flag:
if (preserveCase) {
case_flags.splice(i, 0, input.charCodeAt(ic - 1) - 65 < 26);
}
output.splice(i, 0, n);
i++;
}
if (preserveCase) {
for (i = 0, len = output.length; i < len; i++) {
if (case_flags[i]) {
output[i] = (String.fromCharCode(output[i]).toUpperCase()).charCodeAt(0);
}
}
}
return this.utf16.encode(output);
};
//** Main encode function **
this.encode = function (input, preserveCase) {
//** Bias adaptation function **
var n, delta, h, b, bias, j, m, q, k, t, ijv, case_flags;
if (preserveCase) {
// Preserve case, step1 of 2: Get a list of the unaltered string
case_flags = this.utf16.decode(input);
}
// Converts the input in UTF-16 to Unicode
input = this.utf16.decode(input.toLowerCase());
var input_length = input.length; // Cache the length
if (preserveCase) {
// Preserve case, step2 of 2: Modify the list to true/false
for (j = 0; j < input_length; j++) {
case_flags[j] = input[j] != case_flags[j];
}
}
var output = [];
// Initialize the state:
n = initial_n;
delta = 0;
bias = initial_bias;
// Handle the basic code points:
for (j = 0; j < input_length; ++j) {
if (input[j] < 0x80) {
output.push(
String.fromCharCode(
case_flags ? encode_basic(input[j], case_flags[j]) : input[j]
)
);
}
}
h = b = output.length;
// h is the number of code points that have been handled, b is the
// number of basic code points
if (b > 0) output.push(delimiter);
// Main encoding loop:
//
while (h < input_length) {
// All non-basic code points < n have been
// handled already. Find the next larger one:
for (m = maxint, j = 0; j < input_length; ++j) {
ijv = input[j];
if (ijv >= n && ijv < m) m = ijv;
}
// Increase delta enough to advance the decoder's
// state to , but guard against overflow:
if (m - n > Math.floor((maxint - delta) / (h + 1))) {
throw RangeError("punycode_overflow (1)");
}
delta += (m - n) * (h + 1);
n = m;
for (j = 0; j < input_length; ++j) {
ijv = input[j];
if (ijv < n) {
if (++delta > maxint) return Error("punycode_overflow(2)");
}
if (ijv == n) {
// Represent delta as a generalized variable-length integer:
for (q = delta, k = base; ; k += base) {
t = k <= bias ? tmin : k >= bias + tmax ? tmax : k - bias;
if (q < t) break;
output.push(String.fromCharCode(encode_digit(t + (q - t) % (base - t), 0)));
q = Math.floor((q - t) / (base - t));
}
output.push(String.fromCharCode(encode_digit(q, preserveCase && case_flags[j] ? 1 : 0)));
bias = adapt(delta, h + 1, h == b);
delta = 0;
++h;
}
}
++delta, ++n;
}
return output.join("");
}
this.ToASCII = function (domain) {
var domain_array = domain.split(".");
var out = [];
for (var i = 0; i < domain_array.length; ++i) {
var s = domain_array[i];
out.push(
s.match(/[^A-Za-z0-9-]/) ?
"xn--" + punycode.encode(s) :
s
);
}
return out.join(".");
}
this.ToUnicode = function (domain) {
var domain_array = domain.split(".");
var out = [];
for (var i = 0; i < domain_array.length; ++i) {
var s = domain_array[i];
out.push(
s.match(/^xn--/) ?
punycode.decode(s.slice(4)) :
s
);
}
return out.join(".");
}
}();
Vue.component("http-firewall-block-options-viewer", {
props: ["v-block-options"],
data: function () {
return {
options: this.vBlockOptions
}
},
template: `
默认设置
状态码:{{options.statusCode}} / 提示内容:[{{options.body.length}}字符] [无] / 超时时间:{{options.timeout}}秒
`
})
Vue.component("http-access-log-config-box", {
props: ["v-access-log-config", "v-fields", "v-default-field-codes", "v-is-location", "v-is-group"],
data: function () {
let that = this
// 初始化
setTimeout(function () {
that.changeFields()
}, 100)
let accessLog = {
isPrior: false,
isOn: false,
fields: [1, 2, 6, 7],
status1: true,
status2: true,
status3: true,
status4: true,
status5: true,
firewallOnly: false,
enableClientClosed: false
}
if (this.vAccessLogConfig != null) {
accessLog = this.vAccessLogConfig
}
this.vFields.forEach(function (v) {
if (that.vAccessLogConfig == null) { // 初始化默认值
v.isChecked = that.vDefaultFieldCodes.$contains(v.code)
} else {
v.isChecked = accessLog.fields.$contains(v.code)
}
})
return {
accessLog: accessLog,
hasRequestBodyField: this.vFields.$contains(8),
showAdvancedOptions: false
}
},
methods: {
changeFields: function () {
this.accessLog.fields = this.vFields.filter(function (v) {
return v.isChecked
}).map(function (v) {
return v.code
})
this.hasRequestBodyField = this.accessLog.fields.$contains(8)
},
changeAdvanced: function (v) {
this.showAdvancedOptions = v
}
},
template: ``
})
// 显示流量限制说明
Vue.component("traffic-limit-view", {
props: ["v-traffic-limit"],
data: function () {
return {
config: this.vTrafficLimit
}
},
template: `
日流量限制:{{config.dailySize.count}}{{config.dailySize.unit.toUpperCase()}}
月流量限制:{{config.monthlySize.count}}{{config.monthlySize.unit.toUpperCase()}}
没有限制。
`
})
// 基本认证用户配置
Vue.component("http-auth-basic-auth-user-box", {
props: ["v-users"],
data: function () {
let users = this.vUsers
if (users == null) {
users = []
}
return {
users: users,
isAdding: false,
updatingIndex: -1,
username: "",
password: ""
}
},
methods: {
add: function () {
this.isAdding = true
this.username = ""
this.password = ""
let that = this
setTimeout(function () {
that.$refs.username.focus()
}, 100)
},
cancel: function () {
this.isAdding = false
this.updatingIndex = -1
},
confirm: function () {
let that = this
if (this.username.length == 0) {
teaweb.warn("请输入用户名", function () {
that.$refs.username.focus()
})
return
}
if (this.password.length == 0) {
teaweb.warn("请输入密码", function () {
that.$refs.password.focus()
})
return
}
if (this.updatingIndex < 0) {
this.users.push({
username: this.username,
password: this.password
})
} else {
this.users[this.updatingIndex].username = this.username
this.users[this.updatingIndex].password = this.password
}
this.cancel()
},
update: function (index, user) {
this.updatingIndex = index
this.isAdding = true
this.username = user.username
this.password = user.password
let that = this
setTimeout(function () {
that.$refs.username.focus()
}, 100)
},
remove: function (index) {
this.users.$remove(index)
}
},
template: ``
})
Vue.component("http-location-labels", {
props: ["v-location-config", "v-server-id"],
data: function () {
return {
location: this.vLocationConfig
}
},
methods: {
// 判断是否已启用某配置
configIsOn: function (config) {
return config != null && config.isPrior && config.isOn
},
refIsOn: function (ref, config) {
return this.configIsOn(ref) && config != null && config.isOn
},
len: function (arr) {
return (arr == null) ? 0 : arr.length
},
url: function (path) {
return "/servers/server/settings/locations" + path + "?serverId=" + this.vServerId + "&locationId=" + this.location.id
}
},
template: `
{{location.name}}
{{domain}}
BREAK
自动跳转HTTPS
文档根目录
反向代理
CACHE
{{location.web.charset.charset}}
Gzip:{{location.web.gzip.level}}
请求Header
响应Header
Websocket
请求脚本
访客IP地址
请求限制
PAGE [状态码{{page.status[0]}}] -> {{page.url}}
临时关闭
REWRITE {{rewriteRule.pattern}} -> {{rewriteRule.replace}}
`
})
Vue.component("http-location-labels-label", {
props: ["v-class", "v-href"],
template: ` `
})
Vue.component("http-gzip-box", {
props: ["v-gzip-config", "v-gzip-ref", "v-is-location"],
data: function () {
let gzip = this.vGzipConfig
if (gzip == null) {
gzip = {
isOn: true,
level: 0,
minLength: null,
maxLength: null,
conds: null
}
}
return {
gzip: gzip,
advancedVisible: false
}
},
methods: {
isOn: function () {
return (!this.vIsLocation || this.vGzipRef.isPrior) && this.vGzipRef.isOn
},
changeAdvancedVisible: function (v) {
this.advancedVisible = v
}
},
template: ``
})
Vue.component("script-config-box", {
props: ["id", "v-script-config", "comment"],
data: function () {
let config = this.vScriptConfig
if (config == null) {
config = {
isPrior: false,
isOn: false,
code: ""
}
}
if (config.code.length == 0) {
config.code = "\n\n\n\n"
}
return {
config: config
}
},
watch: {
"config.isOn": function () {
this.change()
}
},
methods: {
change: function () {
this.$emit("change", this.config)
},
changeCode: function (code) {
this.config.code = code
this.change()
}
},
template: `
是否启用
脚本代码
{{config.code}}
`
})
Vue.component("ssl-certs-view", {
props: ["v-certs"],
data: function () {
let certs = this.vCerts
if (certs == null) {
certs = []
}
return {
certs: certs
}
},
methods: {
// 格式化时间
formatTime: function (timestamp) {
return new Date(timestamp * 1000).format("Y-m-d")
},
// 查看详情
viewCert: function (certId) {
teaweb.popup("/servers/certs/certPopup?certId=" + certId, {
height: "28em",
width: "48em"
})
}
},
template: `
{{cert.name}} / {{cert.dnsNames}} / 有效至{{formatTime(cert.timeEndAt)}}
`
})
Vue.component("http-firewall-captcha-options-viewer", {
props: ["v-captcha-options"],
mounted: function () {
this.updateSummary()
},
data: function () {
let options = this.vCaptchaOptions
if (options == null) {
options = {
life: 0,
maxFails: 0,
failBlockTimeout: 0,
failBlockScopeAll: false,
uiIsOn: false,
uiTitle: "",
uiPrompt: "",
uiButtonTitle: "",
uiShowRequestId: false,
uiCss: "",
uiFooter: "",
uiBody: "",
cookieId: "",
lang: ""
}
}
return {
options: options,
summary: ""
}
},
methods: {
updateSummary: function () {
let summaryList = []
if (this.options.life > 0) {
summaryList.push("有效时间" + this.options.life + "秒")
}
if (this.options.maxFails > 0) {
summaryList.push("最多失败" + this.options.maxFails + "次")
}
if (this.options.failBlockTimeout > 0) {
summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒")
}
if (this.options.failBlockScopeAll) {
summaryList.push("全局封禁")
}
if (this.options.uiIsOn) {
summaryList.push("定制UI")
}
if (summaryList.length == 0) {
this.summary = "默认配置"
} else {
this.summary = summaryList.join(" / ")
}
}
},
template: `{{summary}}
`
})
Vue.component("reverse-proxy-box", {
props: ["v-reverse-proxy-ref", "v-reverse-proxy-config", "v-is-location", "v-is-group", "v-family"],
data: function () {
let reverseProxyRef = this.vReverseProxyRef
if (reverseProxyRef == null) {
reverseProxyRef = {
isPrior: false,
isOn: false,
reverseProxyId: 0
}
}
let reverseProxyConfig = this.vReverseProxyConfig
if (reverseProxyConfig == null) {
reverseProxyConfig = {
requestPath: "",
stripPrefix: "",
requestURI: "",
requestHost: "",
requestHostType: 0,
requestHostExcludingPort: false,
addHeaders: [],
connTimeout: {count: 0, unit: "second"},
readTimeout: {count: 0, unit: "second"},
idleTimeout: {count: 0, unit: "second"},
maxConns: 0,
maxIdleConns: 0,
followRedirects: false
}
}
if (reverseProxyConfig.addHeaders == null) {
reverseProxyConfig.addHeaders = []
}
if (reverseProxyConfig.connTimeout == null) {
reverseProxyConfig.connTimeout = {count: 0, unit: "second"}
}
if (reverseProxyConfig.readTimeout == null) {
reverseProxyConfig.readTimeout = {count: 0, unit: "second"}
}
if (reverseProxyConfig.idleTimeout == null) {
reverseProxyConfig.idleTimeout = {count: 0, unit: "second"}
}
if (reverseProxyConfig.proxyProtocol == null) {
// 如果直接赋值Vue将不会触发变更通知
Vue.set(reverseProxyConfig, "proxyProtocol", {
isOn: false,
version: 1
})
}
let forwardHeaders = [
{
name: "X-Real-IP",
isChecked: false
},
{
name: "X-Forwarded-For",
isChecked: false
},
{
name: "X-Forwarded-By",
isChecked: false
},
{
name: "X-Forwarded-Host",
isChecked: false
},
{
name: "X-Forwarded-Proto",
isChecked: false
}
]
forwardHeaders.forEach(function (v) {
v.isChecked = reverseProxyConfig.addHeaders.$contains(v.name)
})
return {
reverseProxyRef: reverseProxyRef,
reverseProxyConfig: reverseProxyConfig,
advancedVisible: false,
family: this.vFamily,
forwardHeaders: forwardHeaders
}
},
watch: {
"reverseProxyConfig.requestHostType": function (v) {
let requestHostType = parseInt(v)
if (isNaN(requestHostType)) {
requestHostType = 0
}
this.reverseProxyConfig.requestHostType = requestHostType
},
"reverseProxyConfig.connTimeout.count": function (v) {
let count = parseInt(v)
if (isNaN(count) || count < 0) {
count = 0
}
this.reverseProxyConfig.connTimeout.count = count
},
"reverseProxyConfig.readTimeout.count": function (v) {
let count = parseInt(v)
if (isNaN(count) || count < 0) {
count = 0
}
this.reverseProxyConfig.readTimeout.count = count
},
"reverseProxyConfig.idleTimeout.count": function (v) {
let count = parseInt(v)
if (isNaN(count) || count < 0) {
count = 0
}
this.reverseProxyConfig.idleTimeout.count = count
},
"reverseProxyConfig.maxConns": function (v) {
let maxConns = parseInt(v)
if (isNaN(maxConns) || maxConns < 0) {
maxConns = 0
}
this.reverseProxyConfig.maxConns = maxConns
},
"reverseProxyConfig.maxIdleConns": function (v) {
let maxIdleConns = parseInt(v)
if (isNaN(maxIdleConns) || maxIdleConns < 0) {
maxIdleConns = 0
}
this.reverseProxyConfig.maxIdleConns = maxIdleConns
},
"reverseProxyConfig.proxyProtocol.version": function (v) {
let version = parseInt(v)
if (isNaN(version)) {
version = 1
}
this.reverseProxyConfig.proxyProtocol.version = version
}
},
methods: {
isOn: function () {
if (this.vIsLocation || this.vIsGroup) {
return this.reverseProxyRef.isPrior && this.reverseProxyRef.isOn
}
return this.reverseProxyRef.isOn
},
changeAdvancedVisible: function (v) {
this.advancedVisible = v
},
changeAddHeader: function () {
this.reverseProxyConfig.addHeaders = this.forwardHeaders.filter(function (v) {
return v.isChecked
}).map(function (v) {
return v.name
})
}
},
template: ``
})
Vue.component("http-firewall-param-filters-box", {
props: ["v-filters"],
data: function () {
let filters = this.vFilters
if (filters == null) {
filters = []
}
return {
filters: filters,
isAdding: false,
options: [
{name: "MD5", code: "md5"},
{name: "URLEncode", code: "urlEncode"},
{name: "URLDecode", code: "urlDecode"},
{name: "BASE64Encode", code: "base64Encode"},
{name: "BASE64Decode", code: "base64Decode"},
{name: "UNICODE编码", code: "unicodeEncode"},
{name: "UNICODE解码", code: "unicodeDecode"},
{name: "HTML实体编码", code: "htmlEscape"},
{name: "HTML实体解码", code: "htmlUnescape"},
{name: "计算长度", code: "length"},
{name: "十六进制->十进制", "code": "hex2dec"},
{name: "十进制->十六进制", "code": "dec2hex"},
{name: "SHA1", "code": "sha1"},
{name: "SHA256", "code": "sha256"}
],
addingCode: ""
}
},
methods: {
add: function () {
this.isAdding = true
this.addingCode = ""
},
confirm: function () {
if (this.addingCode.length == 0) {
return
}
let that = this
this.filters.push(this.options.$find(function (k, v) {
return (v.code == that.addingCode)
}))
this.isAdding = false
},
cancel: function () {
this.isAdding = false
},
remove: function (index) {
this.filters.$remove(index)
}
},
template: ``
})
Vue.component("http-remote-addr-config-box", {
props: ["v-remote-addr-config", "v-is-location", "v-is-group"],
data: function () {
let config = this.vRemoteAddrConfig
if (config == null) {
config = {
isPrior: false,
isOn: false,
value: "${rawRemoteAddr}",
isCustomized: false
}
}
let optionValue = ""
if (!config.isCustomized && (config.value == "${remoteAddr}" || config.value == "${rawRemoteAddr}")) {
optionValue = config.value
}
return {
config: config,
options: [
{
name: "直接获取",
description: "用户直接访问边缘节点,即 \"用户 --> 边缘节点\" 模式,这时候可以直接从连接中读取到真实的IP地址。",
value: "${rawRemoteAddr}"
},
{
name: "从上级代理中获取",
description: "用户和边缘节点之间有别的代理服务转发,即 \"用户 --> [第三方代理服务] --> 边缘节点\",这时候只能从上级代理中获取传递的IP地址。",
value: "${remoteAddr}"
},
{
name: "[自定义]",
description: "通过自定义变量来获取客户端真实的IP地址。",
value: ""
}
],
optionValue: optionValue
}
},
methods: {
isOn: function () {
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
},
changeOptionValue: function () {
if (this.optionValue.length > 0) {
this.config.value = this.optionValue
this.config.isCustomized = false
} else {
this.config.isCustomized = true
}
}
},
template: ``
})
// 访问日志搜索框
Vue.component("http-access-log-search-box", {
props: ["v-ip", "v-domain", "v-keyword", "v-cluster-id", "v-node-id"],
data: function () {
let ip = this.vIp
if (ip == null) {
ip = ""
}
let domain = this.vDomain
if (domain == null) {
domain = ""
}
let keyword = this.vKeyword
if (keyword == null) {
keyword = ""
}
return {
ip: ip,
domain: domain,
keyword: keyword,
clusterId: this.vClusterId
}
},
methods: {
cleanIP: function () {
this.ip = ""
this.submit()
},
cleanDomain: function () {
this.domain = ""
this.submit()
},
cleanKeyword: function () {
this.keyword = ""
this.submit()
},
submit: function () {
let parent = this.$el.parentNode
while (true) {
if (parent == null) {
break
}
if (parent.tagName == "FORM") {
break
}
parent = parent.parentNode
}
if (parent != null) {
setTimeout(function () {
parent.submit()
}, 500)
}
},
changeCluster: function (clusterId) {
this.clusterId = clusterId
}
},
template: ``
})
// 显示指标对象名
Vue.component("metric-key-label", {
props: ["v-key"],
data: function () {
return {
keyDefs: window.METRIC_HTTP_KEYS
}
},
methods: {
keyName: function (key) {
let that = this
let subKey = ""
let def = this.keyDefs.$find(function (k, v) {
if (v.code == key) {
return true
}
if (key.startsWith("${arg.") && v.code.startsWith("${arg.")) {
subKey = that.getSubKey("arg.", key)
return true
}
if (key.startsWith("${header.") && v.code.startsWith("${header.")) {
subKey = that.getSubKey("header.", key)
return true
}
if (key.startsWith("${cookie.") && v.code.startsWith("${cookie.")) {
subKey = that.getSubKey("cookie.", key)
return true
}
return false
})
if (def != null) {
if (subKey.length > 0) {
return def.name + ": " + subKey
}
return def.name
}
return key
},
getSubKey: function (prefix, key) {
prefix = "${" + prefix
let index = key.indexOf(prefix)
if (index >= 0) {
key = key.substring(index + prefix.length)
key = key.substring(0, key.length - 1)
return key
}
return ""
}
},
template: `
{{keyName(this.vKey)}}
`
})
// 指标对象
Vue.component("metric-keys-config-box", {
props: ["v-keys"],
data: function () {
let keys = this.vKeys
if (keys == null) {
keys = []
}
return {
keys: keys,
isAdding: false,
key: "",
subKey: "",
keyDescription: "",
keyDefs: window.METRIC_HTTP_KEYS
}
},
watch: {
keys: function () {
this.$emit("change", this.keys)
}
},
methods: {
cancel: function () {
this.key = ""
this.subKey = ""
this.keyDescription = ""
this.isAdding = false
},
confirm: function () {
if (this.key.length == 0) {
return
}
if (this.key.indexOf(".NAME") > 0) {
if (this.subKey.length == 0) {
teaweb.warn("请输入参数值")
return
}
this.key = this.key.replace(".NAME", "." + this.subKey)
}
this.keys.push(this.key)
this.cancel()
},
add: function () {
this.isAdding = true
let that = this
setTimeout(function () {
if (that.$refs.key != null) {
that.$refs.key.focus()
}
}, 100)
},
remove: function (index) {
this.keys.$remove(index)
},
changeKey: function () {
if (this.key.length == 0) {
return
}
let that = this
let def = this.keyDefs.$find(function (k, v) {
return v.code == that.key
})
if (def != null) {
this.keyDescription = def.description
}
},
keyName: function (key) {
let that = this
let subKey = ""
let def = this.keyDefs.$find(function (k, v) {
if (v.code == key) {
return true
}
if (key.startsWith("${arg.") && v.code.startsWith("${arg.")) {
subKey = that.getSubKey("arg.", key)
return true
}
if (key.startsWith("${header.") && v.code.startsWith("${header.")) {
subKey = that.getSubKey("header.", key)
return true
}
if (key.startsWith("${cookie.") && v.code.startsWith("${cookie.")) {
subKey = that.getSubKey("cookie.", key)
return true
}
return false
})
if (def != null) {
if (subKey.length > 0) {
return def.name + ": " + subKey
}
return def.name
}
return key
},
getSubKey: function (prefix, key) {
prefix = "${" + prefix
let index = key.indexOf(prefix)
if (index >= 0) {
key = key.substring(index + prefix.length)
key = key.substring(0, key.length - 1)
return key
}
return ""
}
},
template: ``
})
Vue.component("http-web-root-box", {
props: ["v-root-config", "v-is-location", "v-is-group"],
data: function () {
let rootConfig = this.vRootConfig
if (rootConfig == null) {
rootConfig = {
isPrior: false,
isOn: true,
dir: "",
indexes: [],
stripPrefix: "",
decodePath: false,
isBreak: false
}
}
if (rootConfig.indexes == null) {
rootConfig.indexes = []
}
return {
rootConfig: rootConfig,
advancedVisible: false
}
},
methods: {
changeAdvancedVisible: function (v) {
this.advancedVisible = v
},
addIndex: function () {
let that = this
teaweb.popup("/servers/server/settings/web/createIndex", {
height: "10em",
callback: function (resp) {
that.rootConfig.indexes.push(resp.data.index)
}
})
},
removeIndex: function (i) {
this.rootConfig.indexes.$remove(i)
},
isOn: function () {
return ((!this.vIsLocation && !this.vIsGroup) || this.rootConfig.isPrior) && this.rootConfig.isOn
}
},
template: ``
})
Vue.component("http-webp-config-box", {
props: ["v-webp-config", "v-is-location", "v-is-group", "v-require-cache"],
data: function () {
let config = this.vWebpConfig
if (config == null) {
config = {
isPrior: false,
isOn: false,
quality: 50,
minLength: {count: 0, "unit": "kb"},
maxLength: {count: 0, "unit": "kb"},
mimeTypes: ["image/png", "image/jpeg", "image/bmp", "image/x-ico", "image/gif"],
extensions: [".png", ".jpeg", ".jpg", ".bmp", ".ico"],
conds: null
}
}
if (config.mimeTypes == null) {
config.mimeTypes = []
}
if (config.extensions == null) {
config.extensions = []
}
return {
config: config,
moreOptionsVisible: false,
quality: config.quality
}
},
watch: {
quality: function (v) {
let quality = parseInt(v)
if (isNaN(quality)) {
quality = 90
} else if (quality < 1) {
quality = 1
} else if (quality > 100) {
quality = 100
}
this.config.quality = quality
}
},
methods: {
isOn: function () {
return ((!this.vIsLocation && !this.vIsGroup) || this.config.isPrior) && this.config.isOn
},
changeExtensions: function (values) {
values.forEach(function (v, k) {
if (v.length > 0 && v[0] != ".") {
values[k] = "." + v
}
})
this.config.extensions = values
},
changeMimeTypes: function (values) {
this.config.mimeTypes = values
},
changeAdvancedVisible: function () {
this.moreOptionsVisible = !this.moreOptionsVisible
},
changeConds: function (conds) {
this.config.conds = conds
}
},
template: ``
})
Vue.component("origin-scheduling-view-box", {
props: ["v-scheduling", "v-params"],
data: function () {
let scheduling = this.vScheduling
if (scheduling == null) {
scheduling = {}
}
return {
scheduling: scheduling
}
},
methods: {
update: function () {
teaweb.popup("/servers/server/settings/reverseProxy/updateSchedulingPopup?" + this.vParams, {
height: "21em",
callback: function () {
window.location.reload()
},
})
}
},
template: `
当前正在使用的算法
{{scheduling.name}} [修改]
`
})
Vue.component("http-firewall-block-options", {
props: ["v-block-options"],
data: function () {
return {
blockOptions: this.vBlockOptions,
statusCode: this.vBlockOptions.statusCode,
timeout: this.vBlockOptions.timeout,
isEditing: false
}
},
watch: {
statusCode: function (v) {
let statusCode = parseInt(v)
if (isNaN(statusCode)) {
this.blockOptions.statusCode = 403
} else {
this.blockOptions.statusCode = statusCode
}
},
timeout: function (v) {
let timeout = parseInt(v)
if (isNaN(timeout)) {
this.blockOptions.timeout = 0
} else {
this.blockOptions.timeout = timeout
}
}
},
methods: {
edit: function () {
this.isEditing = !this.isEditing
}
},
template: `
`
})
Vue.component("http-request-cond-view", {
props: ["v-cond"],
data: function () {
return {
cond: this.vCond,
components: window.REQUEST_COND_COMPONENTS
}
},
methods: {
typeName: function (cond) {
let c = this.components.$find(function (k, v) {
return v.type == cond.type
})
if (c != null) {
return c.name;
}
return cond.param + " " + cond.operator
},
updateConds: function (conds, simpleCond) {
for (let k in simpleCond) {
if (simpleCond.hasOwnProperty(k)) {
this.cond[k] = simpleCond[k]
}
}
},
notifyChange: function () {
}
},
template: `
{{cond.param}} {{cond.operator}}
{{typeName(cond)}}:
{{cond.value}}
`
})
Vue.component("http-firewall-rules-box", {
props: ["v-rules", "v-type"],
data: function () {
let rules = this.vRules
if (rules == null) {
rules = []
}
return {
rules: rules
}
},
methods: {
addRule: function () {
window.UPDATING_RULE = null
let that = this
teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, {
callback: function (resp) {
that.rules.push(resp.data.rule)
}
})
},
updateRule: function (index, rule) {
window.UPDATING_RULE = rule
let that = this
teaweb.popup("/servers/components/waf/createRulePopup?type=" + this.vType, {
callback: function (resp) {
Vue.set(that.rules, index, resp.data.rule)
}
})
},
removeRule: function (index) {
let that = this
teaweb.confirm("确定要删除此规则吗?", function () {
that.rules.$remove(index)
})
}
},
template: `
{{rule.name}}[{{rule.param}}]
{{rule.checkpointOptions.period}}秒/{{rule.checkpointOptions.threshold}}请求
{{rule.checkpointOptions.allowDomains}}
| {{paramFilter.code}} {{rule.operator}} {{rule.value}}
({{rule.description}})
+
`
})
Vue.component("http-fastcgi-box", {
props: ["v-fastcgi-ref", "v-fastcgi-configs", "v-is-location"],
data: function () {
let fastcgiRef = this.vFastcgiRef
if (fastcgiRef == null) {
fastcgiRef = {
isPrior: false,
isOn: false,
fastcgiIds: []
}
}
let fastcgiConfigs = this.vFastcgiConfigs
if (fastcgiConfigs == null) {
fastcgiConfigs = []
} else {
fastcgiRef.fastcgiIds = fastcgiConfigs.map(function (v) {
return v.id
})
}
return {
fastcgiRef: fastcgiRef,
fastcgiConfigs: fastcgiConfigs,
advancedVisible: false
}
},
methods: {
isOn: function () {
return (!this.vIsLocation || this.fastcgiRef.isPrior) && this.fastcgiRef.isOn
},
createFastcgi: function () {
let that = this
teaweb.popup("/servers/server/settings/fastcgi/createPopup", {
height: "26em",
callback: function (resp) {
teaweb.success("添加成功", function () {
that.fastcgiConfigs.push(resp.data.fastcgi)
that.fastcgiRef.fastcgiIds.push(resp.data.fastcgi.id)
})
}
})
},
updateFastcgi: function (fastcgiId, index) {
let that = this
teaweb.popup("/servers/server/settings/fastcgi/updatePopup?fastcgiId=" + fastcgiId, {
callback: function (resp) {
teaweb.success("修改成功", function () {
Vue.set(that.fastcgiConfigs, index, resp.data.fastcgi)
})
}
})
},
removeFastcgi: function (index) {
this.fastcgiRef.fastcgiIds.$remove(index)
this.fastcgiConfigs.$remove(index)
}
},
template: ``
})
// 请求方法列表
Vue.component("http-methods-box", {
props: ["v-methods"],
data: function () {
let methods = this.vMethods
if (methods == null) {
methods = []
}
return {
methods: methods,
isAdding: false,
addingMethod: ""
}
},
methods: {
add: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.addingMethod.focus()
}, 100)
},
confirm: function () {
let that = this
// 删除其中的空格
this.addingMethod = this.addingMethod.replace(/\s/g, "").toUpperCase()
if (this.addingMethod.length == 0) {
teaweb.warn("请输入要添加的请求方法", function () {
that.$refs.addingMethod.focus()
})
return
}
// 是否已经存在
if (this.methods.$contains(this.addingMethod)) {
teaweb.warn("此请求方法已经存在,无需重复添加", function () {
that.$refs.addingMethod.focus()
})
return
}
this.methods.push(this.addingMethod)
this.cancel()
},
remove: function (index) {
this.methods.$remove(index)
},
cancel: function () {
this.isAdding = false
this.addingMethod = ""
}
},
template: ``
})
// URL扩展名条件
Vue.component("http-cond-url-extension", {
props: ["v-cond"],
data: function () {
let cond = {
isRequest: true,
param: "${requestPathExtension}",
operator: "in",
value: "[]"
}
if (this.vCond != null && this.vCond.param == cond.param) {
cond.value = this.vCond.value
}
let extensions = []
try {
extensions = JSON.parse(cond.value)
} catch (e) {
}
return {
cond: cond,
extensions: extensions, // TODO 可以拖动排序
isAdding: false,
addingExt: ""
}
},
watch: {
extensions: function () {
this.cond.value = JSON.stringify(this.extensions)
}
},
methods: {
addExt: function () {
this.isAdding = !this.isAdding
if (this.isAdding) {
let that = this
setTimeout(function () {
that.$refs.addingExt.focus()
}, 100)
}
},
cancelAdding: function () {
this.isAdding = false
this.addingExt = ""
},
confirmAdding: function () {
// TODO 做更详细的校验
// TODO 如果有重复的则提示之
if (this.addingExt.length == 0) {
return
}
if (this.addingExt[0] != ".") {
this.addingExt = "." + this.addingExt
}
this.addingExt = this.addingExt.replace(/\s+/g, "").toLowerCase()
this.extensions.push(this.addingExt)
// 清除状态
this.cancelAdding()
},
removeExt: function (index) {
this.extensions.$remove(index)
}
},
template: ``
})
// 排除URL扩展名条件
Vue.component("http-cond-url-not-extension", {
props: ["v-cond"],
data: function () {
let cond = {
isRequest: true,
param: "${requestPathExtension}",
operator: "not in",
value: "[]"
}
if (this.vCond != null && this.vCond.param == cond.param) {
cond.value = this.vCond.value
}
let extensions = []
try {
extensions = JSON.parse(cond.value)
} catch (e) {
}
return {
cond: cond,
extensions: extensions, // TODO 可以拖动排序
isAdding: false,
addingExt: ""
}
},
watch: {
extensions: function () {
this.cond.value = JSON.stringify(this.extensions)
}
},
methods: {
addExt: function () {
this.isAdding = !this.isAdding
if (this.isAdding) {
let that = this
setTimeout(function () {
that.$refs.addingExt.focus()
}, 100)
}
},
cancelAdding: function () {
this.isAdding = false
this.addingExt = ""
},
confirmAdding: function () {
// TODO 做更详细的校验
// TODO 如果有重复的则提示之
if (this.addingExt.length == 0) {
return
}
if (this.addingExt[0] != ".") {
this.addingExt = "." + this.addingExt
}
this.addingExt = this.addingExt.replace(/\s+/g, "").toLowerCase()
this.extensions.push(this.addingExt)
// 清除状态
this.cancelAdding()
},
removeExt: function (index) {
this.extensions.$remove(index)
}
},
template: ``
})
// 根据URL前缀
Vue.component("http-cond-url-prefix", {
props: ["v-cond"],
mounted: function () {
this.$refs.valueInput.focus()
},
data: function () {
let cond = {
isRequest: true,
param: "${requestPath}",
operator: "prefix",
value: "",
isCaseInsensitive: false
}
if (this.vCond != null && typeof (this.vCond.value) == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `
`
})
Vue.component("http-cond-url-not-prefix", {
props: ["v-cond"],
mounted: function () {
this.$refs.valueInput.focus()
},
data: function () {
let cond = {
isRequest: true,
param: "${requestPath}",
operator: "prefix",
value: "",
isReverse: true,
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `
`
})
// 首页
Vue.component("http-cond-url-eq-index", {
props: ["v-cond"],
data: function () {
let cond = {
isRequest: true,
param: "${requestPath}",
operator: "eq",
value: "/",
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `
`
})
// URL精准匹配
Vue.component("http-cond-url-eq", {
props: ["v-cond"],
mounted: function () {
this.$refs.valueInput.focus()
},
data: function () {
let cond = {
isRequest: true,
param: "${requestPath}",
operator: "eq",
value: "",
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `
`
})
Vue.component("http-cond-url-not-eq", {
props: ["v-cond"],
mounted: function () {
this.$refs.valueInput.focus()
},
data: function () {
let cond = {
isRequest: true,
param: "${requestPath}",
operator: "eq",
value: "",
isReverse: true,
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `
`
})
// URL正则匹配
Vue.component("http-cond-url-regexp", {
props: ["v-cond"],
mounted: function () {
this.$refs.valueInput.focus()
},
data: function () {
let cond = {
isRequest: true,
param: "${requestPath}",
operator: "regexp",
value: "",
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `
`
})
// 排除URL正则匹配
Vue.component("http-cond-url-not-regexp", {
props: ["v-cond"],
mounted: function () {
this.$refs.valueInput.focus()
},
data: function () {
let cond = {
isRequest: true,
param: "${requestPath}",
operator: "not regexp",
value: "",
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `
`
})
// User-Agent正则匹配
Vue.component("http-cond-user-agent-regexp", {
props: ["v-cond"],
mounted: function () {
this.$refs.valueInput.focus()
},
data: function () {
let cond = {
isRequest: true,
param: "${userAgent}",
operator: "regexp",
value: "",
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `
`
})
// User-Agent正则不匹配
Vue.component("http-cond-user-agent-not-regexp", {
props: ["v-cond"],
mounted: function () {
this.$refs.valueInput.focus()
},
data: function () {
let cond = {
isRequest: true,
param: "${userAgent}",
operator: "not regexp",
value: "",
isCaseInsensitive: false
}
if (this.vCond != null && typeof this.vCond.value == "string") {
cond.value = this.vCond.value
}
return {
cond: cond
}
},
methods: {
changeCaseInsensitive: function (isCaseInsensitive) {
this.cond.isCaseInsensitive = isCaseInsensitive
}
},
template: `
`
})
// 根据MimeType
Vue.component("http-cond-mime-type", {
props: ["v-cond"],
data: function () {
let cond = {
isRequest: false,
param: "${response.contentType}",
operator: "mime type",
value: "[]"
}
if (this.vCond != null && this.vCond.param == cond.param) {
cond.value = this.vCond.value
}
return {
cond: cond,
mimeTypes: JSON.parse(cond.value), // TODO 可以拖动排序
isAdding: false,
addingMimeType: ""
}
},
watch: {
mimeTypes: function () {
this.cond.value = JSON.stringify(this.mimeTypes)
}
},
methods: {
addMimeType: function () {
this.isAdding = !this.isAdding
if (this.isAdding) {
let that = this
setTimeout(function () {
that.$refs.addingMimeType.focus()
}, 100)
}
},
cancelAdding: function () {
this.isAdding = false
this.addingMimeType = ""
},
confirmAdding: function () {
// TODO 做更详细的校验
// TODO 如果有重复的则提示之
if (this.addingMimeType.length == 0) {
return
}
this.addingMimeType = this.addingMimeType.replace(/\s+/g, "")
this.mimeTypes.push(this.addingMimeType)
// 清除状态
this.cancelAdding()
},
removeMimeType: function (index) {
this.mimeTypes.$remove(index)
}
},
template: ``
})
// 参数匹配
Vue.component("http-cond-params", {
props: ["v-cond"],
mounted: function () {
let cond = this.vCond
if (cond == null) {
return
}
this.operator = cond.operator
// stringValue
if (["regexp", "not regexp", "eq", "not", "prefix", "suffix", "contains", "not contains", "eq ip", "gt ip", "gte ip", "lt ip", "lte ip", "ip range"].$contains(cond.operator)) {
this.stringValue = cond.value
return
}
// numberValue
if (["eq int", "eq float", "gt", "gte", "lt", "lte", "mod 10", "ip mod 10", "mod 100", "ip mod 100"].$contains(cond.operator)) {
this.numberValue = cond.value
return
}
// modValue
if (["mod", "ip mod"].$contains(cond.operator)) {
let pieces = cond.value.split(",")
this.modDivValue = pieces[0]
if (pieces.length > 1) {
this.modRemValue = pieces[1]
}
return
}
// stringValues
let that = this
if (["in", "not in", "file ext", "mime type"].$contains(cond.operator)) {
try {
let arr = JSON.parse(cond.value)
if (arr != null && (arr instanceof Array)) {
arr.forEach(function (v) {
that.stringValues.push(v)
})
}
} catch (e) {
}
return
}
// versionValue
if (["version range"].$contains(cond.operator)) {
let pieces = cond.value.split(",")
this.versionRangeMinValue = pieces[0]
if (pieces.length > 1) {
this.versionRangeMaxValue = pieces[1]
}
return
}
},
data: function () {
let cond = {
isRequest: true,
param: "",
operator: window.REQUEST_COND_OPERATORS[0].op,
value: "",
isCaseInsensitive: false
}
if (this.vCond != null) {
cond = this.vCond
}
return {
cond: cond,
operators: window.REQUEST_COND_OPERATORS,
operator: window.REQUEST_COND_OPERATORS[0].op,
operatorDescription: window.REQUEST_COND_OPERATORS[0].description,
variables: window.REQUEST_VARIABLES,
variable: "",
// 各种类型的值
stringValue: "",
numberValue: "",
modDivValue: "",
modRemValue: "",
stringValues: [],
versionRangeMinValue: "",
versionRangeMaxValue: ""
}
},
methods: {
changeVariable: function () {
let v = this.cond.param
if (v == null) {
v = ""
}
this.cond.param = v + this.variable
},
changeOperator: function () {
let that = this
this.operators.forEach(function (v) {
if (v.op == that.operator) {
that.operatorDescription = v.description
}
})
this.cond.operator = this.operator
// 移动光标
let box = document.getElementById("variables-value-box")
if (box != null) {
setTimeout(function () {
let input = box.getElementsByTagName("INPUT")
if (input.length > 0) {
input[0].focus()
}
}, 100)
}
},
changeStringValues: function (v) {
this.stringValues = v
this.cond.value = JSON.stringify(v)
}
},
watch: {
stringValue: function (v) {
this.cond.value = v
},
numberValue: function (v) {
// TODO 校验数字
this.cond.value = v
},
modDivValue: function (v) {
if (v.length == 0) {
return
}
let div = parseInt(v)
if (isNaN(div)) {
div = 1
}
this.modDivValue = div
this.cond.value = div + "," + this.modRemValue
},
modRemValue: function (v) {
if (v.length == 0) {
return
}
let rem = parseInt(v)
if (isNaN(rem)) {
rem = 0
}
this.modRemValue = rem
this.cond.value = this.modDivValue + "," + rem
},
versionRangeMinValue: function (v) {
this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue
},
versionRangeMaxValue: function (v) {
this.cond.value = this.versionRangeMinValue + "," + this.versionRangeMaxValue
}
},
template: `
参数值
操作符
{{operator.name}}
对比值
不区分大小写
`
})
// 请求方法列表
Vue.component("http-status-box", {
props: ["v-status-list"],
data: function () {
let statusList = this.vStatusList
if (statusList == null) {
statusList = []
}
return {
statusList: statusList,
isAdding: false,
addingStatus: ""
}
},
methods: {
add: function () {
this.isAdding = true
let that = this
setTimeout(function () {
that.$refs.addingStatus.focus()
}, 100)
},
confirm: function () {
let that = this
// 删除其中的空格
this.addingStatus = this.addingStatus.replace(/\s/g, "").toUpperCase()
if (this.addingStatus.length == 0) {
teaweb.warn("请输入要添加的状态码", function () {
that.$refs.addingStatus.focus()
})
return
}
// 是否已经存在
if (this.statusList.$contains(this.addingStatus)) {
teaweb.warn("此状态码已经存在,无需重复添加", function () {
that.$refs.addingStatus.focus()
})
return
}
// 格式
if (!this.addingStatus.match(/^\d{3}$/)) {
teaweb.warn("请输入正确的状态码", function () {
that.$refs.addingStatus.focus()
})
return
}
this.statusList.push(parseInt(this.addingStatus, 10))
this.cancel()
},
remove: function (index) {
this.statusList.$remove(index)
},
cancel: function () {
this.isAdding = false
this.addingStatus = ""
}
},
template: ``
})
Vue.component("server-group-selector", {
props: ["v-groups"],
data: function () {
let groups = this.vGroups
if (groups == null) {
groups = []
}
return {
groups: groups
}
},
methods: {
selectGroup: function () {
let that = this
let groupIds = this.groups.map(function (v) {
return v.id.toString()
}).join(",")
teaweb.popup("/servers/groups/selectPopup?selectedGroupIds=" + groupIds, {
callback: function (resp) {
that.groups.push(resp.data.group)
}
})
},
addGroup: function () {
let that = this
teaweb.popup("/servers/groups/createPopup", {
callback: function (resp) {
that.groups.push(resp.data.group)
}
})
},
removeGroup: function (index) {
this.groups.$remove(index)
},
groupIds: function () {
return this.groups.map(function (v) {
return v.id
})
}
},
template: ``
})
Vue.component("script-group-config-box", {
props: ["v-group", "v-is-location"],
data: function () {
let group = this.vGroup
if (group == null) {
group = {
isPrior: false,
isOn: true,
scripts: []
}
}
if (group.scripts == null) {
group.scripts = []
}
let script = null
if (group.scripts.length > 0) {
script = group.scripts[group.scripts.length - 1]
}
return {
group: group,
script: script
}
},
methods: {
changeScript: function (script) {
this.group.scripts = [script] // 目前只支持单个脚本
this.change()
},
change: function () {
this.$emit("change", this.group)
}
},
template: ``
})
// 指标周期设置
Vue.component("metric-period-config-box", {
props: ["v-period", "v-period-unit"],
data: function () {
let period = this.vPeriod
let periodUnit = this.vPeriodUnit
if (period == null || period.toString().length == 0) {
period = 1
}
if (periodUnit == null || periodUnit.length == 0) {
periodUnit = "day"
}
return {
periodConfig: {
period: period,
unit: periodUnit
}
}
},
watch: {
"periodConfig.period": function (v) {
v = parseInt(v)
if (isNaN(v) || v <= 0) {
v = 1
}
this.periodConfig.period = v
}
},
template: ``
})
Vue.component("traffic-limit-config-box", {
props: ["v-traffic-limit"],
data: function () {
let config = this.vTrafficLimit
if (config == null) {
config = {
isOn: false,
dailySize: {
count: -1,
unit: "gb"
},
monthlySize: {
count: -1,
unit: "gb"
},
totalSize: {
count: -1,
unit: "gb"
},
noticePageBody: ""
}
}
if (config.dailySize == null) {
config.dailySize = {
count: -1,
unit: "gb"
}
}
if (config.monthlySize == null) {
config.monthlySize = {
count: -1,
unit: "gb"
}
}
if (config.totalSize == null) {
config.totalSize = {
count: -1,
unit: "gb"
}
}
return {
config: config
}
},
methods: {
showBodyTemplate: function () {
this.config.noticePageBody = `
Traffic Limit Exceeded Warning
Traffic Limit Exceeded Warning
The site traffic has exceeded the limit. Please contact with the site administrator.
Request ID: \${requestId}.
`
}
},
template: ``
})
Vue.component("http-firewall-captcha-options", {
props: ["v-captcha-options"],
mounted: function () {
this.updateSummary()
},
data: function () {
let options = this.vCaptchaOptions
if (options == null) {
options = {
countLetters: 0,
life: 0,
maxFails: 0,
failBlockTimeout: 0,
failBlockScopeAll: false,
uiIsOn: false,
uiTitle: "",
uiPrompt: "",
uiButtonTitle: "",
uiShowRequestId: true,
uiCss: "",
uiFooter: "",
uiBody: "",
cookieId: "",
lang: ""
}
}
if (options.countLetters <= 0) {
options.countLetters = 6
}
return {
options: options,
isEditing: false,
summary: ""
}
},
watch: {
"options.countLetters": function (v) {
let i = parseInt(v, 10)
if (isNaN(i)) {
i = 0
} else if (i < 0) {
i = 0
} else if (i > 10) {
i = 10
}
this.options.countLetters = i
},
"options.life": function (v) {
let i = parseInt(v, 10)
if (isNaN(i)) {
i = 0
}
this.options.life = i
this.updateSummary()
},
"options.maxFails": function (v) {
let i = parseInt(v, 10)
if (isNaN(i)) {
i = 0
}
this.options.maxFails = i
this.updateSummary()
},
"options.failBlockTimeout": function (v) {
let i = parseInt(v, 10)
if (isNaN(i)) {
i = 0
}
this.options.failBlockTimeout = i
this.updateSummary()
},
"options.failBlockScopeAll": function (v) {
this.updateSummary()
},
"options.uiIsOn": function (v) {
this.updateSummary()
}
},
methods: {
edit: function () {
this.isEditing = !this.isEditing
},
updateSummary: function () {
let summaryList = []
if (this.options.life > 0) {
summaryList.push("有效时间" + this.options.life + "秒")
}
if (this.options.maxFails > 0) {
summaryList.push("最多失败" + this.options.maxFails + "次")
}
if (this.options.failBlockTimeout > 0) {
summaryList.push("失败拦截" + this.options.failBlockTimeout + "秒")
}
if (this.options.failBlockScopeAll) {
summaryList.push("全局封禁")
}
if (this.options.uiIsOn) {
summaryList.push("定制UI")
}
if (summaryList.length == 0) {
this.summary = "默认配置"
} else {
this.summary = summaryList.join(" / ")
}
},
confirm: function () {
this.isEditing = false
}
},
template: `
`
})
Vue.component("firewall-syn-flood-config-box", {
props: ["v-syn-flood-config"],
data: function () {
let config = this.vSynFloodConfig
if (config == null) {
config = {
isOn: false,
minAttempts: 10,
timeoutSeconds: 600,
ignoreLocal: true
}
}
return {
config: config,
isEditing: false,
minAttempts: config.minAttempts,
timeoutSeconds: config.timeoutSeconds
}
},
methods: {
edit: function () {
this.isEditing = !this.isEditing
}
},
watch: {
minAttempts: function (v) {
let count = parseInt(v)
if (isNaN(count)) {
count = 10
}
if (count < 5) {
count = 5
}
this.config.minAttempts = count
},
timeoutSeconds: function (v) {
let seconds = parseInt(v)
if (isNaN(seconds)) {
seconds = 10
}
if (seconds < 60) {
seconds = 60
}
this.config.timeoutSeconds = seconds
}
},
template: ``
})
// TODO 支持关键词搜索
// TODO 改成弹窗选择
Vue.component("admin-selector", {
props: ["v-admin-id"],
mounted: function () {
let that = this
Tea.action("/admins/options")
.post()
.success(function (resp) {
that.admins = resp.data.admins
})
},
data: function () {
let adminId = this.vAdminId
if (adminId == null) {
adminId = 0
}
return {
admins: [],
adminId: adminId
}
},
template: `
[选择系统用户]
{{admin.name}}({{admin.username}})
`
})
// 绑定IP列表
Vue.component("ip-list-bind-box", {
props: ["v-http-firewall-policy-id", "v-type"],
mounted: function () {
this.refresh()
},
data: function () {
return {
policyId: this.vHttpFirewallPolicyId,
type: this.vType,
lists: []
}
},
methods: {
bind: function () {
let that = this
teaweb.popup("/servers/iplists/bindHTTPFirewallPopup?httpFirewallPolicyId=" + this.policyId + "&type=" + this.type, {
width: "50em",
height: "34em",
callback: function () {
},
onClose: function () {
that.refresh()
}
})
},
remove: function (index, listId) {
let that = this
teaweb.confirm("确定要删除这个绑定的IP名单吗?", function () {
Tea.action("/servers/iplists/unbindHTTPFirewall")
.params({
httpFirewallPolicyId: that.policyId,
listId: listId
})
.post()
.success(function (resp) {
that.lists.$remove(index)
})
})
},
refresh: function () {
let that = this
Tea.action("/servers/iplists/httpFirewall")
.params({
httpFirewallPolicyId: this.policyId,
type: this.vType
})
.post()
.success(function (resp) {
that.lists = resp.data.lists
})
}
},
template: ``
})
Vue.component("ip-list-table", {
props: ["v-items", "v-keyword", "v-show-search-button"],
data: function () {
return {
items: this.vItems,
keyword: (this.vKeyword != null) ? this.vKeyword : "",
selectedAll: false,
hasSelectedItems: false
}
},
methods: {
updateItem: function (itemId) {
this.$emit("update-item", itemId)
},
deleteItem: function (itemId) {
this.$emit("delete-item", itemId)
},
viewLogs: function (itemId) {
teaweb.popup("/servers/iplists/accessLogsPopup?itemId=" + itemId, {
width: "50em",
height: "30em"
})
},
changeSelectedAll: function () {
let boxes = this.$refs.itemCheckBox
if (boxes == null) {
return
}
let that = this
boxes.forEach(function (box) {
box.checked = that.selectedAll
})
this.hasSelectedItems = this.selectedAll
},
changeSelected: function (e) {
let that = this
that.hasSelectedItems = false
let boxes = that.$refs.itemCheckBox
if (boxes == null) {
return
}
boxes.forEach(function (box) {
if (box.checked) {
that.hasSelectedItems = true
}
})
},
deleteAll: function () {
let boxes = this.$refs.itemCheckBox
if (boxes == null) {
return
}
let itemIds = []
boxes.forEach(function (box) {
if (box.checked) {
itemIds.push(box.value)
}
})
if (itemIds.length == 0) {
return
}
Tea.action("/servers/iplists/deleteItems")
.post()
.params({
itemIds: itemIds
})
.success(function () {
teaweb.successToast("批量删除成功", 1200, teaweb.reload)
})
},
formatSeconds: function (seconds) {
if (seconds < 60) {
return seconds + "秒"
}
if (seconds < 3600) {
return Math.ceil(seconds / 60) + "分钟"
}
if (seconds < 86400) {
return Math.ceil(seconds / 3600) + "小时"
}
return Math.ceil(seconds / 86400) + "天"
}
},
template: `
IP
类型
级别
过期时间
备注
操作
{{item.ipFrom}} New
- {{item.ipTo}}
*
{{item.region}}
| {{item.isp}}
IPv4
IPv4
IPv6
所有IP
{{item.eventLevelName}}
-
{{item.expiredTime}}
已过期
{{formatSeconds(item.lifeSeconds)}}
不过期
{{item.reason}}
-
日志
修改
删除
`
})
Vue.component("ip-item-text", {
props: ["v-item"],
template: `
*
{{vItem.ipFrom}}
- {{vItem.ipTo}}
{{vItem.ipFrom}}
级别:{{vItem.eventLevelName}}
`
})
Vue.component("ip-box", {
props: ["v-ip"],
methods: {
popup: function () {
let ip = this.vIp
if (ip == null || ip.length == 0) {
let e = this.$refs.container
ip = e.innerText
if (ip == null) {
ip = e.textContent
}
}
teaweb.popup("/servers/ipbox?ip=" + ip, {
width: "50em",
height: "30em"
})
}
},
template: ` `
})
Vue.component("api-node-selector", {
props: [],
data: function () {
return {}
},
template: `
暂未实现
`
})
Vue.component("api-node-addresses-box", {
props: ["v-addrs", "v-name"],
data: function () {
let addrs = this.vAddrs
if (addrs == null) {
addrs = []
}
return {
addrs: addrs
}
},
methods: {
// 添加IP地址
addAddr: function () {
let that = this;
teaweb.popup("/api/node/createAddrPopup", {
height: "16em",
callback: function (resp) {
that.addrs.push(resp.data.addr);
}
})
},
// 修改地址
updateAddr: function (index, addr) {
let that = this;
window.UPDATING_ADDR = addr
teaweb.popup("/api/node/updateAddrPopup?addressId=", {
callback: function (resp) {
Vue.set(that.addrs, index, resp.data.addr);
}
})
},
// 删除IP地址
removeAddr: function (index) {
this.addrs.$remove(index);
}
},
template: `
{{addr.protocol}}://{{addr.host.quoteIP()}}:{{addr.portRange}}
+
`
})
// 给Table增加排序功能
function sortTable(callback) {
// 引入js
let jsFile = document.createElement("script")
jsFile.setAttribute("src", "/js/sortable.min.js")
jsFile.addEventListener("load", function () {
// 初始化
let box = document.querySelector("#sortable-table")
if (box == null) {
return
}
Sortable.create(box, {
draggable: "tbody",
handle: ".icon.handle",
onStart: function () {
},
onUpdate: function (event) {
let rows = box.querySelectorAll("tbody")
let rowIds = []
rows.forEach(function (row) {
rowIds.push(parseInt(row.getAttribute("v-id")))
})
callback(rowIds)
}
})
})
document.head.appendChild(jsFile)
}
function sortLoad(callback) {
let jsFile = document.createElement("script")
jsFile.setAttribute("src", "/js/sortable.min.js")
jsFile.addEventListener("load", function () {
if (typeof (callback) == "function") {
callback()
}
})
document.head.appendChild(jsFile)
}
Vue.component("page-box", {
data: function () {
return {
page: ""
}
},
created: function () {
let that = this;
setTimeout(function () {
that.page = Tea.Vue.page;
})
},
template: ``
})
Vue.component("network-addresses-box", {
props: ["v-server-type", "v-addresses", "v-protocol", "v-name", "v-from", "v-support-range", "v-url"],
data: function () {
let addresses = this.vAddresses
if (addresses == null) {
addresses = []
}
let protocol = this.vProtocol
if (protocol == null) {
protocol = ""
}
let name = this.vName
if (name == null) {
name = "addresses"
}
let from = this.vFrom
if (from == null) {
from = ""
}
return {
addresses: addresses,
protocol: protocol,
name: name,
from: from
}
},
watch: {
"vServerType": function () {
this.addresses = []
},
"vAddresses": function () {
if (this.vAddresses != null) {
this.addresses = this.vAddresses
}
}
},
methods: {
addAddr: function () {
let that = this
window.UPDATING_ADDR = null
let url = this.vUrl
if (url == null) {
url = "/servers/addPortPopup"
}
teaweb.popup(url + "?serverType=" + this.vServerType + "&protocol=" + this.protocol + "&from=" + this.from + "&supportRange=" + (this.supportRange() ? 1 : 0), {
height: "18em",
callback: function (resp) {
var addr = resp.data.address
if (that.addresses.$find(function (k, v) {
return addr.host == v.host && addr.portRange == v.portRange && addr.protocol == v.protocol
}) != null) {
teaweb.warn("要添加的网络地址已经存在")
return
}
that.addresses.push(addr)
if (["https", "https4", "https6"].$contains(addr.protocol)) {
this.tlsProtocolName = "HTTPS"
} else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) {
this.tlsProtocolName = "TLS"
}
// 发送事件
that.$emit("change", that.addresses)
}
})
},
removeAddr: function (index) {
this.addresses.$remove(index);
// 发送事件
this.$emit("change", this.addresses)
},
updateAddr: function (index, addr) {
let that = this
window.UPDATING_ADDR = addr
let url = this.vUrl
if (url == null) {
url = "/servers/addPortPopup"
}
teaweb.popup(url + "?serverType=" + this.vServerType + "&protocol=" + this.protocol + "&from=" + this.from + "&supportRange=" + (this.supportRange() ? 1 : 0), {
height: "18em",
callback: function (resp) {
var addr = resp.data.address
Vue.set(that.addresses, index, addr)
if (["https", "https4", "https6"].$contains(addr.protocol)) {
this.tlsProtocolName = "HTTPS"
} else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) {
this.tlsProtocolName = "TLS"
}
// 发送事件
that.$emit("change", that.addresses)
}
})
// 发送事件
this.$emit("change", this.addresses)
},
supportRange: function () {
return this.vSupportRange || (this.vServerType == "tcpProxy" || this.vServerType == "udpProxy")
}
},
template: `
{{addr.protocol}}://
{{addr.host.quoteIP()}} * :
{{addr.portRange}} {{addr.portRange}}
[添加端口绑定]
`
})
/**
* 保存按钮
*/
Vue.component("submit-btn", {
template: '保存 '
});
// 可以展示更多条目的角图表
Vue.component("more-items-angle", {
props: ["v-data-url", "v-url"],
data: function () {
return {
visible: false
}
},
methods: {
show: function () {
this.visible = !this.visible
if (this.visible) {
this.showBox()
} else {
this.hideBox()
}
},
showBox: function () {
let that = this
this.visible = true
Tea.action(this.vDataUrl)
.params({
url: this.vUrl
})
.post()
.success(function (resp) {
let groups = resp.data.groups
let boxLeft = that.$el.offsetLeft + 120;
let boxTop = that.$el.offsetTop + 70;
let box = document.createElement("div")
box.setAttribute("id", "more-items-box")
box.style.cssText = "z-index: 100; position: absolute; left: " + boxLeft + "px; top: " + boxTop + "px; max-height: 30em; overflow: auto; border-bottom: 1px solid rgba(34,36,38,.15)"
document.body.append(box)
let menuHTML = ""
box.innerHTML = menuHTML
let listener = function (e) {
if (e.target.tagName == "I") {
return
}
if (!that.isInBox(box, e.target)) {
document.removeEventListener("click", listener)
that.hideBox()
}
}
document.addEventListener("click", listener)
})
},
hideBox: function () {
let box = document.getElementById("more-items-box")
if (box != null) {
box.parentNode.removeChild(box)
}
this.visible = false
},
isInBox: function (parent, child) {
while (true) {
if (child == null) {
break
}
if (child.parentNode == parent) {
return true
}
child = child.parentNode
}
return false
}
},
template: ` `
})
/**
* 菜单项
*/
Vue.component("menu-item", {
props: ["href", "active", "code"],
data: function () {
let active = this.active
if (typeof (active) == "undefined") {
var itemCode = ""
if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") {
itemCode = window.TEA.ACTION.data.firstMenuItem
}
if (itemCode != null && itemCode.length > 0 && this.code != null && this.code.length > 0) {
if (itemCode.indexOf(",") > 0) {
active = itemCode.split(",").$contains(this.code)
} else {
active = (itemCode == this.code)
}
}
}
let href = (this.href == null) ? "" : this.href
if (typeof (href) == "string" && href.length > 0 && href.startsWith(".")) {
let qIndex = href.indexOf("?")
if (qIndex >= 0) {
href = Tea.url(href.substring(0, qIndex)) + href.substring(qIndex)
} else {
href = Tea.url(href)
}
}
return {
vHref: href,
vActive: active
}
},
methods: {
click: function (e) {
this.$emit("click", e)
}
},
template: '\
\
'
});
// 使用Icon的链接方式
Vue.component("link-icon", {
props: ["href", "title", "target"],
data: function () {
return {
vTitle: (this.title == null) ? "打开链接" : this.title
}
},
template: ` `
})
// 带有下划虚线的连接
Vue.component("link-red", {
props: ["href", "title"],
data: function () {
let href = this.href
if (href == null) {
href = ""
}
return {
vHref: href
}
},
methods: {
clickPrevent: function () {
emitClick(this, arguments)
}
},
template: ` `
})
// 会弹出窗口的链接
Vue.component("link-popup", {
props: ["title"],
methods: {
clickPrevent: function () {
emitClick(this, arguments)
}
},
template: ` `
})
Vue.component("popup-icon", {
props: ["title", "href", "height"],
methods: {
clickPrevent: function () {
if (this.href != null && this.href.length > 0) {
teaweb.popup(this.href, {
height: this.height
})
}
}
},
template: ` `
})
// 小提示
Vue.component("tip-icon", {
props: ["content"],
methods: {
showTip: function () {
teaweb.popupTip(this.content)
}
},
template: ` `
})
// 提交点击事件
function emitClick(obj, arguments) {
let event = "click"
let newArgs = [event]
for (let i = 0; i < arguments.length; i++) {
newArgs.push(arguments[i])
}
obj.$emit.apply(obj, newArgs)
}
Vue.component("countries-selector", {
props: ["v-countries"],
data: function () {
let countries = this.vCountries
if (countries == null) {
countries = []
}
let countryIds = countries.$map(function (k, v) {
return v.id
})
return {
countries: countries,
countryIds: countryIds
}
},
methods: {
add: function () {
let countryStringIds = this.countryIds.map(function (v) {
return v.toString()
})
let that = this
teaweb.popup("/ui/selectCountriesPopup?countryIds=" + countryStringIds.join(","), {
width: "48em",
height: "23em",
callback: function (resp) {
that.countries = resp.data.countries
that.change()
}
})
},
remove: function (index) {
this.countries.$remove(index)
this.change()
},
change: function () {
this.countryIds = this.countries.$map(function (k, v) {
return v.id
})
}
},
template: ``
})
Vue.component("raquo-item", {
template: `» `
})
Vue.component("more-options-tbody", {
data: function () {
return {
isVisible: false
}
},
methods: {
show: function () {
this.isVisible = !this.isVisible
this.$emit("change", this.isVisible)
}
},
template: `
更多选项 收起选项
`
})
Vue.component("download-link", {
props: ["v-element", "v-file", "v-value"],
created: function () {
let that = this
setTimeout(function () {
that.url = that.composeURL()
}, 1000)
},
data: function () {
let filename = this.vFile
if (filename == null || filename.length == 0) {
filename = "unknown-file"
}
return {
file: filename,
url: this.composeURL()
}
},
methods: {
composeURL: function () {
let text = ""
if (this.vValue != null) {
text = this.vValue
} else {
let e = document.getElementById(this.vElement)
if (e == null) {
teaweb.warn("找不到要下载的内容")
return
}
text = e.innerText
if (text == null) {
text = e.textContent
}
}
return Tea.url("/ui/download", {
file: this.file,
text: text
})
}
},
template: ` `,
})
Vue.component("values-box", {
props: ["values", "v-values", "size", "maxlength", "name", "placeholder"],
data: function () {
let values = this.values;
if (values == null) {
values = [];
}
if (this.vValues != null && typeof this.vValues == "object") {
values = this.vValues
}
return {
"realValues": values,
"isUpdating": false,
"isAdding": false,
"index": 0,
"value": "",
isEditing: false
}
},
methods: {
create: function () {
this.isAdding = true;
var that = this;
setTimeout(function () {
that.$refs.value.focus();
}, 200);
},
update: function (index) {
this.cancel()
this.isUpdating = true;
this.index = index;
this.value = this.realValues[index];
var that = this;
setTimeout(function () {
that.$refs.value.focus();
}, 200);
},
confirm: function () {
if (this.value.length == 0) {
return
}
if (this.isUpdating) {
Vue.set(this.realValues, this.index, this.value);
} else {
this.realValues.push(this.value);
}
this.cancel()
this.$emit("change", this.realValues)
},
remove: function (index) {
this.realValues.$remove(index)
this.$emit("change", this.realValues)
},
cancel: function () {
this.isUpdating = false;
this.isAdding = false;
this.value = "";
},
updateAll: function (values) {
this.realValues = values
},
addValue: function (v) {
this.realValues.push(v)
},
startEditing: function () {
this.isEditing = !this.isEditing
},
allValues: function () {
return this.realValues
}
},
template: ``
});
Vue.component("datetime-input", {
props: ["v-name", "v-timestamp"],
mounted: function () {
let that = this
teaweb.datepicker(this.$refs.dayInput, function (v) {
that.day = v
that.hour = "23"
that.minute = "59"
that.second = "59"
that.change()
})
},
data: function () {
let timestamp = this.vTimestamp
if (timestamp != null) {
timestamp = parseInt(timestamp)
if (isNaN(timestamp)) {
timestamp = 0
}
} else {
timestamp = 0
}
let day = ""
let hour = ""
let minute = ""
let second = ""
if (timestamp > 0) {
let date = new Date()
date.setTime(timestamp * 1000)
let year = date.getFullYear().toString()
let month = this.leadingZero((date.getMonth() + 1).toString(), 2)
day = year + "-" + month + "-" + this.leadingZero(date.getDate().toString(), 2)
hour = this.leadingZero(date.getHours().toString(), 2)
minute = this.leadingZero(date.getMinutes().toString(), 2)
second = this.leadingZero(date.getSeconds().toString(), 2)
}
return {
timestamp: timestamp,
day: day,
hour: hour,
minute: minute,
second: second,
hasDayError: false,
hasHourError: false,
hasMinuteError: false,
hasSecondError: false
}
},
methods: {
change: function () {
let date = new Date()
// day
if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(this.day)) {
this.hasDayError = true
return
}
let pieces = this.day.split("-")
let year = parseInt(pieces[0])
date.setFullYear(year)
let month = parseInt(pieces[1])
if (month < 1 || month > 12) {
this.hasDayError = true
return
}
date.setMonth(month - 1)
let day = parseInt(pieces[2])
if (day < 1 || day > 32) {
this.hasDayError = true
return
}
date.setDate(day)
this.hasDayError = false
// hour
if (!/^\d+$/.test(this.hour)) {
this.hasHourError = true
return
}
let hour = parseInt(this.hour)
if (isNaN(hour)) {
this.hasHourError = true
return
}
if (hour < 0 || hour >= 24) {
this.hasHourError = true
return
}
this.hasHourError = false
date.setHours(hour)
// minute
if (!/^\d+$/.test(this.minute)) {
this.hasMinuteError = true
return
}
let minute = parseInt(this.minute)
if (isNaN(minute)) {
this.hasMinuteError = true
return
}
if (minute < 0 || minute >= 60) {
this.hasMinuteError = true
return
}
this.hasMinuteError = false
date.setMinutes(minute)
// second
if (!/^\d+$/.test(this.second)) {
this.hasSecondError = true
return
}
let second = parseInt(this.second)
if (isNaN(second)) {
this.hasSecondError = true
return
}
if (second < 0 || second >= 60) {
this.hasSecondError = true
return
}
this.hasSecondError = false
date.setSeconds(second)
this.timestamp = Math.floor(date.getTime() / 1000)
},
leadingZero: function (s, l) {
s = s.toString()
if (l <= s.length) {
return s
}
for (let i = 0; i < l - s.length; i++) {
s = "0" + s
}
return s
},
resultTimestamp: function () {
return this.timestamp
},
nextDays: function (days) {
let date = new Date()
date.setTime(date.getTime() + days * 86400 * 1000)
this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2)
this.hour = this.leadingZero(date.getHours(), 2)
this.minute = this.leadingZero(date.getMinutes(), 2)
this.second = this.leadingZero(date.getSeconds(), 2)
this.change()
},
nextHours: function (hours) {
let date = new Date()
date.setTime(date.getTime() + hours * 3600 * 1000)
this.day = date.getFullYear() + "-" + this.leadingZero(date.getMonth() + 1, 2) + "-" + this.leadingZero(date.getDate(), 2)
this.hour = this.leadingZero(date.getHours(), 2)
this.minute = this.leadingZero(date.getMinutes(), 2)
this.second = this.leadingZero(date.getSeconds(), 2)
this.change()
}
},
template: ``
})
// 启用状态标签
Vue.component("label-on", {
props: ["v-is-on"],
template: '已启用 已停用
'
})
// 文字代码标签
Vue.component("code-label", {
methods: {
click: function (args) {
this.$emit("click", args)
}
},
template: ` `
})
Vue.component("code-label-plain", {
template: ` `
})
// tiny标签
Vue.component("tiny-label", {
template: ` `
})
Vue.component("tiny-basic-label", {
template: ` `
})
// 更小的标签
Vue.component("micro-basic-label", {
template: ` `
})
// 灰色的Label
Vue.component("grey-label", {
props: ["color"],
data: function () {
let color = "grey"
if (this.color != null && this.color.length > 0) {
color = "red"
}
return {
labelColor: color
}
},
template: ` `
})
// 可选标签
Vue.component("optional-label", {
template: `(可选) `
})
// Plus专属
Vue.component("plus-label", {
template: `Plus专属功能。 `
})
// 提醒设置项为专业设置
Vue.component("pro-warning-label", {
template: ` 注意:通常不需要修改;如要修改,请在专家指导下进行。 `
})
/**
* 一级菜单
*/
Vue.component("first-menu", {
props: [],
template: ' \
'
});
/**
* 更多选项
*/
Vue.component("more-options-indicator", {
data: function () {
return {
visible: false
}
},
methods: {
changeVisible: function () {
this.visible = !this.visible
if (Tea.Vue != null) {
Tea.Vue.moreOptionsVisible = this.visible
}
this.$emit("change", this.visible)
}
},
template: '更多选项 收起选项 '
});
Vue.component("page-size-selector", {
data: function () {
let query = window.location.search
let pageSize = 10
if (query.length > 0) {
query = query.substr(1)
let params = query.split("&")
params.forEach(function (v) {
let pieces = v.split("=")
if (pieces.length == 2 && pieces[0] == "pageSize") {
let pageSizeString = pieces[1]
if (pageSizeString.match(/^\d+$/)) {
pageSize = parseInt(pageSizeString, 10)
if (isNaN(pageSize) || pageSize < 1) {
pageSize = 10
}
}
}
})
}
return {
pageSize: pageSize
}
},
watch: {
pageSize: function () {
window.ChangePageSize(this.pageSize)
}
},
template: `
\t[每页] 10条 20条 30条 40条 50条 60条 70条 80条 90条 100条
`
})
/**
* 二级菜单
*/
Vue.component("second-menu", {
template: ' \
'
});
Vue.component("loading-message", {
template: ``
})
Vue.component("more-options-angle", {
data: function () {
return {
isVisible: false
}
},
methods: {
show: function () {
this.isVisible = !this.isVisible
this.$emit("change", this.isVisible)
}
},
template: `更多选项 收起选项 `
})
/**
* 菜单项
*/
Vue.component("inner-menu-item", {
props: ["href", "active", "code"],
data: function () {
var active = this.active;
if (typeof(active) =="undefined") {
var itemCode = "";
if (typeof (window.TEA.ACTION.data.firstMenuItem) != "undefined") {
itemCode = window.TEA.ACTION.data.firstMenuItem;
}
active = (itemCode == this.code);
}
return {
vHref: (this.href == null) ? "" : this.href,
vActive: active
};
},
template: '\
[ ] \
'
});
Vue.component("health-check-config-box", {
props: ["v-health-check-config"],
data: function () {
let healthCheckConfig = this.vHealthCheckConfig
let urlProtocol = "http"
let urlPort = ""
let urlRequestURI = "/"
let urlHost = ""
if (healthCheckConfig == null) {
healthCheckConfig = {
isOn: false,
url: "",
interval: {count: 60, unit: "second"},
statusCodes: [200],
timeout: {count: 10, unit: "second"},
countTries: 3,
tryDelay: {count: 100, unit: "ms"},
autoDown: true,
countUp: 1,
countDown: 3,
userAgent: "",
onlyBasicRequest: true,
accessLogIsOn: true
}
let that = this
setTimeout(function () {
that.changeURL()
}, 500)
} else {
try {
let url = new URL(healthCheckConfig.url)
urlProtocol = url.protocol.substring(0, url.protocol.length - 1)
// 域名
urlHost = url.host
if (urlHost == "%24%7Bhost%7D") {
urlHost = "${host}"
}
let colonIndex = urlHost.indexOf(":")
if (colonIndex > 0) {
urlHost = urlHost.substring(0, colonIndex)
}
urlPort = url.port
urlRequestURI = url.pathname
if (url.search.length > 0) {
urlRequestURI += url.search
}
} catch (e) {
}
if (healthCheckConfig.statusCodes == null) {
healthCheckConfig.statusCodes = [200]
}
if (healthCheckConfig.interval == null) {
healthCheckConfig.interval = {count: 60, unit: "second"}
}
if (healthCheckConfig.timeout == null) {
healthCheckConfig.timeout = {count: 10, unit: "second"}
}
if (healthCheckConfig.tryDelay == null) {
healthCheckConfig.tryDelay = {count: 100, unit: "ms"}
}
if (healthCheckConfig.countUp == null || healthCheckConfig.countUp < 1) {
healthCheckConfig.countUp = 1
}
if (healthCheckConfig.countDown == null || healthCheckConfig.countDown < 1) {
healthCheckConfig.countDown = 3
}
}
return {
healthCheck: healthCheckConfig,
advancedVisible: false,
urlProtocol: urlProtocol,
urlHost: urlHost,
urlPort: urlPort,
urlRequestURI: urlRequestURI,
urlIsEditing: healthCheckConfig.url.length == 0
}
},
watch: {
urlRequestURI: function () {
if (this.urlRequestURI.length > 0 && this.urlRequestURI[0] != "/") {
this.urlRequestURI = "/" + this.urlRequestURI
}
this.changeURL()
},
urlPort: function (v) {
let port = parseInt(v)
if (!isNaN(port)) {
this.urlPort = port.toString()
} else {
this.urlPort = ""
}
this.changeURL()
},
urlProtocol: function () {
this.changeURL()
},
urlHost: function () {
this.changeURL()
},
"healthCheck.countTries": function (v) {
let count = parseInt(v)
if (!isNaN(count)) {
this.healthCheck.countTries = count
} else {
this.healthCheck.countTries = 0
}
},
"healthCheck.countUp": function (v) {
let count = parseInt(v)
if (!isNaN(count)) {
this.healthCheck.countUp = count
} else {
this.healthCheck.countUp = 0
}
},
"healthCheck.countDown": function (v) {
let count = parseInt(v)
if (!isNaN(count)) {
this.healthCheck.countDown = count
} else {
this.healthCheck.countDown = 0
}
}
},
methods: {
showAdvanced: function () {
this.advancedVisible = !this.advancedVisible
},
changeURL: function () {
let urlHost = this.urlHost
if (urlHost.length == 0) {
urlHost = "${host}"
}
this.healthCheck.url = this.urlProtocol + "://" + urlHost + ((this.urlPort.length > 0) ? ":" + this.urlPort : "") + this.urlRequestURI
},
changeStatus: function (values) {
this.healthCheck.statusCodes = values.$map(function (k, v) {
let status = parseInt(v)
if (isNaN(status)) {
return 0
} else {
return status
}
})
},
editURL: function () {
this.urlIsEditing = !this.urlIsEditing
}
},
template: ``
})
// 将变量转换为中文
Vue.component("request-variables-describer", {
data: function () {
return {
vars:[]
}
},
methods: {
update: function (variablesString) {
this.vars = []
let that = this
variablesString.replace(/\${.+?}/g, function (v) {
let def = that.findVar(v)
if (def == null) {
return v
}
that.vars.push(def)
})
},
findVar: function (name) {
let def = null
window.REQUEST_VARIABLES.forEach(function (v) {
if (v.code == name) {
def = v
}
})
return def
}
},
template: `
{{v.code}} - {{v.name}};
`
})
Vue.component("combo-box", {
// data-url 和 data-key 成对出现
props: [
"name", "title", "placeholder", "size", "v-items", "v-value",
"data-url", // 数据源URL
"data-key", // 数据源中数据的键名
"data-search", // 是否启用动态搜索,如果值为on或true,则表示启用
"width"
],
mounted: function () {
if (this.dataURL.length > 0) {
this.search("")
}
// 设定菜单宽度
let searchBox = this.$refs.searchBox
if (searchBox != null) {
let inputWidth = searchBox.offsetWidth
if (inputWidth != null && inputWidth > 0) {
this.$refs.menu.style.width = inputWidth + "px"
} else if (this.styleWidth.length > 0) {
this.$refs.menu.style.width = this.styleWidth
}
}
},
data: function () {
let items = this.vItems
if (items == null || !(items instanceof Array)) {
items = []
}
items = this.formatItems(items)
// 当前选中项
let selectedItem = null
if (this.vValue != null) {
let that = this
items.forEach(function (v) {
if (v.value == that.vValue) {
selectedItem = v
}
})
}
let width = this.width
if (width == null || width.length == 0) {
width = "11em"
} else {
if (/\d+$/.test(width)) {
width += "em"
}
}
// data url
let dataURL = ""
if (typeof this.dataUrl == "string" && this.dataUrl.length > 0) {
dataURL = this.dataUrl
}
return {
allItems: items, // 原始的所有的items
items: items.$copy(), // 候选的items
selectedItem: selectedItem, // 选中的item
keyword: "",
visible: false,
hideTimer: null,
hoverIndex: 0,
styleWidth: width,
isInitial: true,
dataURL: dataURL,
urlRequestId: 0 // 记录URL请求ID,防止并行冲突
}
},
methods: {
search: function (keyword) {
// 从URL中获取选项数据
let dataUrl = this.dataURL
let dataKey = this.dataKey
let that = this
let requestId = Math.random()
this.urlRequestId = requestId
Tea.action(dataUrl)
.params({
keyword: (keyword == null) ? "" : keyword
})
.post()
.success(function (resp) {
if (requestId != that.urlRequestId) {
return
}
if (resp.data != null) {
if (typeof (resp.data[dataKey]) == "object") {
let items = that.formatItems(resp.data[dataKey])
that.allItems = items
that.items = items.$copy()
if (that.isInitial) {
that.isInitial = false
if (that.vValue != null) {
items.forEach(function (v) {
if (v.value == that.vValue) {
that.selectedItem = v
}
})
}
}
}
}
})
},
formatItems: function (items) {
items.forEach(function (v) {
if (v.value == null) {
v.value = v.id
}
})
return items
},
reset: function () {
this.selectedItem = null
this.change()
this.hoverIndex = 0
let that = this
setTimeout(function () {
if (that.$refs.searchBox) {
that.$refs.searchBox.focus()
}
})
},
clear: function () {
this.selectedItem = null
this.change()
this.hoverIndex = 0
},
changeKeyword: function () {
let shouldSearch = this.dataURL.length > 0 && (this.dataSearch == "on" || this.dataSearch == "true")
this.hoverIndex = 0
let keyword = this.keyword
if (keyword.length == 0) {
if (shouldSearch) {
this.search(keyword)
} else {
this.items = this.allItems.$copy()
}
return
}
if (shouldSearch) {
this.search(keyword)
} else {
this.items = this.allItems.$copy().filter(function (v) {
if (v.fullname != null && v.fullname.length > 0 && teaweb.match(v.fullname, keyword)) {
return true
}
return teaweb.match(v.name, keyword)
})
}
},
selectItem: function (item) {
this.selectedItem = item
this.change()
this.hoverIndex = 0
this.keyword = ""
this.changeKeyword()
},
confirm: function () {
if (this.items.length > this.hoverIndex) {
this.selectItem(this.items[this.hoverIndex])
}
},
show: function () {
this.visible = true
// 不要重置hoverIndex,以便焦点可以在输入框和可选项之间切换
},
hide: function () {
let that = this
this.hideTimer = setTimeout(function () {
that.visible = false
}, 500)
},
downItem: function () {
this.hoverIndex++
if (this.hoverIndex > this.items.length - 1) {
this.hoverIndex = 0
}
this.focusItem()
},
upItem: function () {
this.hoverIndex--
if (this.hoverIndex < 0) {
this.hoverIndex = 0
}
this.focusItem()
},
focusItem: function () {
if (this.hoverIndex < this.items.length) {
this.$refs.itemRef[this.hoverIndex].focus()
let that = this
setTimeout(function () {
that.$refs.searchBox.focus()
if (that.hideTimer != null) {
clearTimeout(that.hideTimer)
that.hideTimer = null
}
})
}
},
change: function () {
this.$emit("change", this.selectedItem)
let that = this
setTimeout(function () {
if (that.$refs.selectedLabel != null) {
that.$refs.selectedLabel.focus()
}
})
},
submitForm: function (event) {
if (event.target.tagName != "A") {
return
}
let parentBox = this.$refs.selectedLabel.parentNode
while (true) {
parentBox = parentBox.parentNode
if (parentBox == null || parentBox.tagName == "BODY") {
return
}
if (parentBox.tagName == "FORM") {
parentBox.submit()
break
}
}
},
setDataURL: function (dataURL) {
this.dataURL = dataURL
},
reloadData: function () {
this.search("")
}
},
template: ``
})
Vue.component("dot", {
template: ' '
})
Vue.component("time-duration-box", {
props: ["v-name", "v-value", "v-count", "v-unit"],
mounted: function () {
this.change()
},
data: function () {
let v = this.vValue
if (v == null) {
v = {
count: this.vCount,
unit: this.vUnit
}
}
if (typeof (v["count"]) != "number") {
v["count"] = -1
}
return {
duration: v,
countString: (v.count >= 0) ? v.count.toString() : ""
}
},
watch: {
"countString": function (newValue) {
let value = newValue.trim()
if (value.length == 0) {
this.duration.count = -1
return
}
let count = parseInt(value)
if (!isNaN(count)) {
this.duration.count = count
}
this.change()
}
},
methods: {
change: function () {
this.$emit("change", this.duration)
}
},
template: ``
})
Vue.component("not-found-box", {
props: ["message"],
template: ``
})
// 警告消息
Vue.component("warning-message", {
template: ``
})
let checkboxId = 0
Vue.component("checkbox", {
props: ["name", "value", "v-value", "id", "checked"],
data: function () {
checkboxId++
let elementId = this.id
if (elementId == null) {
elementId = "checkbox" + checkboxId
}
let elementValue = this.vValue
if (elementValue == null) {
elementValue = "1"
}
let checkedValue = this.value
if (checkedValue == null && this.checked == "checked") {
checkedValue = elementValue
}
return {
elementId: elementId,
elementValue: elementValue,
newValue: checkedValue
}
},
methods: {
change: function () {
this.$emit("input", this.newValue)
},
check: function () {
this.newValue = this.elementValue
},
uncheck: function () {
this.newValue = ""
},
isChecked: function () {
return this.newValue == this.elementValue
}
},
watch: {
value: function (v) {
if (typeof v == "boolean") {
this.newValue = v
}
}
},
template: `
`
})
Vue.component("network-addresses-view", {
props: ["v-addresses"],
template: `
{{addr.protocol}}://{{addr.host.quoteIP()}} * :{{addr.portRange}}
`
})
Vue.component("size-capacity-view", {
props:["v-default-text", "v-value"],
template: `
{{vValue.count}}{{vValue.unit.toUpperCase()}}
{{vDefaultText}}
`
})
// 信息提示窗口
Vue.component("tip-message-box", {
props: ["code"],
mounted: function () {
let that = this
Tea.action("/ui/showTip")
.params({
code: this.code
})
.success(function (resp) {
that.visible = resp.data.visible
})
.post()
},
data: function () {
return {
visible: false
}
},
methods: {
close: function () {
this.visible = false
Tea.action("/ui/hideTip")
.params({
code: this.code
})
.post()
}
},
template: ``
})
Vue.component("digit-input", {
props: ["value", "maxlength", "size", "min", "max", "required", "placeholder"],
mounted: function () {
let that = this
setTimeout(function () {
that.check()
})
},
data: function () {
let realMaxLength = this.maxlength
if (realMaxLength == null) {
realMaxLength = 20
}
let realSize = this.size
if (realSize == null) {
realSize = 6
}
return {
realValue: this.value,
realMaxLength: realMaxLength,
realSize: realSize,
isValid: true
}
},
watch: {
realValue: function (v) {
this.notifyChange()
}
},
methods: {
notifyChange: function () {
let v = parseInt(this.realValue.toString(), 10)
if (isNaN(v)) {
v = 0
}
this.check()
this.$emit("input", v)
},
check: function () {
if (this.realValue == null) {
return
}
let s = this.realValue.toString()
if (!/^\d+$/.test(s)) {
this.isValid = false
return
}
let v = parseInt(s, 10)
if (isNaN(v)) {
this.isValid = false
} else {
if (this.required) {
this.isValid = (this.min == null || this.min <= v) && (this.max == null || this.max >= v)
} else {
this.isValid = (v == 0 || (this.min == null || this.min <= v) && (this.max == null || this.max >= v))
}
}
}
},
template: ` `
})
Vue.component("keyword", {
props: ["v-word"],
data: function () {
let word = this.vWord
if (word == null) {
word = ""
} else {
word = word.replace(/\)/g, "\\)")
word = word.replace(/\(/g, "\\(")
word = word.replace(/\+/g, "\\+")
word = word.replace(/\^/g, "\\^")
word = word.replace(/\$/g, "\\$")
word = word.replace(/\?/, "\\?")
word = word.replace(/\*/, "\\*")
word = word.replace(/\[/, "\\[")
word = word.replace(/{/, "\\{")
word = word.replace(/\./, "\\.")
}
let slot = this.$slots["default"][0]
let text = slot.text
if (word.length > 0) {
let that = this
let m = [] // replacement => tmp
let tmpIndex = 0
text = text.replaceAll(new RegExp("(" + word + ")", "ig"), function (replacement) {
tmpIndex++
let s = "" + that.encodeHTML(replacement) + " "
let tmpKey = "$TMP__KEY__" + tmpIndex.toString() + "$"
m.push([tmpKey, s])
return tmpKey
})
text = this.encodeHTML(text)
m.forEach(function (r) {
text = text.replace(r[0], r[1])
})
} else {
text = this.encodeHTML(text)
}
return {
word: word,
text: text
}
},
methods: {
encodeHTML: function (s) {
s = s.replace(/&/g, "&")
s = s.replace(//g, ">")
s = s.replace(/"/g, """)
return s
}
},
template: ` `
})
Vue.component("node-log-row", {
props: ["v-log", "v-keyword"],
data: function () {
return {
log: this.vLog,
keyword: this.vKeyword
}
},
template: `
[{{log.createdTime}}] [{{log.createdTime}}] [{{log.tag}}]{{log.description}} 共{{log.count}}条 {{log.server.name}}
`
})
Vue.component("provinces-selector", {
props: ["v-provinces"],
data: function () {
let provinces = this.vProvinces
if (provinces == null) {
provinces = []
}
let provinceIds = provinces.$map(function (k, v) {
return v.id
})
return {
provinces: provinces,
provinceIds: provinceIds
}
},
methods: {
add: function () {
let provinceStringIds = this.provinceIds.map(function (v) {
return v.toString()
})
let that = this
teaweb.popup("/ui/selectProvincesPopup?provinceIds=" + provinceStringIds.join(","), {
width: "48em",
height: "23em",
callback: function (resp) {
that.provinces = resp.data.provinces
that.change()
}
})
},
remove: function (index) {
this.provinces.$remove(index)
this.change()
},
change: function () {
this.provinceIds = this.provinces.$map(function (k, v) {
return v.id
})
}
},
template: ``
})
Vue.component("csrf-token", {
created: function () {
this.refreshToken()
},
mounted: function () {
let that = this
this.$refs.token.form.addEventListener("submit", function () {
that.refreshToken()
})
// 自动刷新
setInterval(function () {
that.refreshToken()
}, 10 * 60 * 1000)
},
data: function () {
return {
token: ""
}
},
methods: {
refreshToken: function () {
let that = this
Tea.action("/csrf/token")
.get()
.success(function (resp) {
that.token = resp.data.token
})
}
},
template: ` `
})
Vue.component("labeled-input", {
props: ["name", "size", "maxlength", "label", "value"],
template: ' \
\
{{label}} \
'
});
let radioId = 0
Vue.component("radio", {
props: ["name", "value", "v-value", "id"],
data: function () {
radioId++
let elementId = this.id
if (elementId == null) {
elementId = "radio" + radioId
}
return {
"elementId": elementId
}
},
methods: {
change: function () {
this.$emit("input", this.vValue)
}
},
template: `
`
})
Vue.component("copy-to-clipboard", {
props: ["v-target"],
created: function () {
if (typeof ClipboardJS == "undefined") {
let jsFile = document.createElement("script")
jsFile.setAttribute("src", "/js/clipboard.min.js")
document.head.appendChild(jsFile)
}
},
methods: {
copy: function () {
new ClipboardJS('[data-clipboard-target]');
teaweb.successToast("已复制到剪切板")
}
},
template: ` `
})
// 节点角色名称
Vue.component("node-role-name", {
props: ["v-role"],
data: function () {
let roleName = ""
switch (this.vRole) {
case "node":
roleName = "边缘节点"
break
case "monitor":
roleName = "监控节点"
break
case "api":
roleName = "API节点"
break
case "user":
roleName = "用户平台"
break
case "admin":
roleName = "管理平台"
break
case "database":
roleName = "数据库节点"
break
case "dns":
roleName = "DNS节点"
break
case "report":
roleName = "区域监控终端"
break
}
return {
roleName: roleName
}
},
template: `{{roleName}} `
})
let sourceCodeBoxIndex = 0
Vue.component("source-code-box", {
props: ["name", "type", "id", "read-only", "width", "height", "focus"],
mounted: function () {
let readOnly = this.readOnly
if (typeof readOnly != "boolean") {
readOnly = true
}
let box = document.getElementById("source-code-box-" + this.index)
let valueBox = document.getElementById(this.valueBoxId)
let value = ""
if (valueBox.textContent != null) {
value = valueBox.textContent
} else if (valueBox.innerText != null) {
value = valueBox.innerText
}
this.createEditor(box, value, readOnly)
},
data: function () {
let index = sourceCodeBoxIndex++
let valueBoxId = 'source-code-box-value-' + sourceCodeBoxIndex
if (this.id != null) {
valueBoxId = this.id
}
return {
index: index,
valueBoxId: valueBoxId
}
},
methods: {
createEditor: function (box, value, readOnly) {
let boxEditor = CodeMirror.fromTextArea(box, {
theme: "idea",
lineNumbers: true,
value: "",
readOnly: readOnly,
showCursorWhenSelecting: true,
height: "auto",
//scrollbarStyle: null,
viewportMargin: Infinity,
lineWrapping: true,
highlightFormatting: false,
indentUnit: 4,
indentWithTabs: true,
})
let that = this
boxEditor.on("change", function () {
that.change(boxEditor.getValue())
})
boxEditor.setValue(value)
if (this.focus) {
boxEditor.focus()
}
let width = this.width
let height = this.height
if (width != null && height != null) {
width = parseInt(width)
height = parseInt(height)
if (!isNaN(width) && !isNaN(height)) {
if (width <= 0) {
width = box.parentNode.offsetWidth
}
boxEditor.setSize(width, height)
}
} else if (height != null) {
height = parseInt(height)
if (!isNaN(height)) {
boxEditor.setSize("100%", height)
}
}
let info = CodeMirror.findModeByMIME(this.type)
if (info != null) {
boxEditor.setOption("mode", info.mode)
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js"
CodeMirror.autoLoadMode(boxEditor, info.mode)
}
},
change: function (code) {
this.$emit("change", code)
}
},
template: ``
})
Vue.component("size-capacity-box", {
props: ["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"],
data: function () {
let v = this.vValue
if (v == null) {
v = {
count: this.vCount,
unit: this.vUnit
}
}
if (typeof (v["count"]) != "number") {
v["count"] = -1
}
let vSize = this.size
if (vSize == null) {
vSize = 6
}
let vMaxlength = this.maxlength
if (vMaxlength == null) {
vMaxlength = 10
}
let supportedUnits = this.vSupportedUnits
if (supportedUnits == null) {
supportedUnits = []
}
return {
capacity: v,
countString: (v.count >= 0) ? v.count.toString() : "",
vSize: vSize,
vMaxlength: vMaxlength,
supportedUnits: supportedUnits
}
},
watch: {
"countString": function (newValue) {
let value = newValue.trim()
if (value.length == 0) {
this.capacity.count = -1
this.change()
return
}
let count = parseInt(value)
if (!isNaN(count)) {
this.capacity.count = count
}
this.change()
}
},
methods: {
change: function () {
this.$emit("change", this.capacity)
}
},
template: ``
})
/**
* 二级菜单
*/
Vue.component("inner-menu", {
template: `
`
});
Vue.component("datepicker", {
props: ["v-name", "v-value", "v-bottom-left"],
mounted: function () {
let that = this
teaweb.datepicker(this.$refs.dayInput, function (v) {
that.day = v
that.change()
}, !!this.vBottomLeft)
},
data: function () {
let name = this.vName
if (name == null) {
name = "day"
}
let day = this.vValue
if (day == null) {
day = ""
}
return {
name: name,
day: day
}
},
methods: {
change: function () {
this.$emit("change", this.day)
}
},
template: `
`
})
// 排序使用的箭头
Vue.component("sort-arrow", {
props: ["name"],
data: function () {
let url = window.location.toString()
let order = ""
let iconTitle = ""
let newArgs = []
if (window.location.search != null && window.location.search.length > 0) {
let queryString = window.location.search.substring(1)
let pieces = queryString.split("&")
let that = this
pieces.forEach(function (v) {
let eqIndex = v.indexOf("=")
if (eqIndex > 0) {
let argName = v.substring(0, eqIndex)
let argValue = v.substring(eqIndex + 1)
if (argName == that.name) {
order = argValue
} else if (argName != "page" && argValue != "asc" && argValue != "desc") {
newArgs.push(v)
}
} else {
newArgs.push(v)
}
})
}
if (order == "asc") {
newArgs.push(this.name + "=desc")
iconTitle = "当前正序排列"
} else if (order == "desc") {
newArgs.push(this.name + "=asc")
iconTitle = "当前倒序排列"
} else {
newArgs.push(this.name + "=desc")
iconTitle = "当前正序排列"
}
let qIndex = url.indexOf("?")
if (qIndex > 0) {
url = url.substring(0, qIndex) + "?" + newArgs.join("&")
} else {
url = url + "?" + newArgs.join("&")
}
return {
order: order,
url: url,
iconTitle: iconTitle
}
},
template: ` `
})
Vue.component("user-link", {
props: ["v-user", "v-keyword"],
data: function () {
let user = this.vUser
if (user == null) {
user = {id: 0, "username": "", "fullname": ""}
}
return {
user: user
}
},
template: `
{{user.fullname}} ({{user.username}} )
[已删除]
`
})
// 监控节点分组选择
Vue.component("report-node-groups-selector", {
props: ["v-group-ids"],
mounted: function () {
let that = this
Tea.action("/clusters/monitors/groups/options")
.post()
.success(function (resp) {
that.groups = resp.data.groups.map(function (group) {
group.isChecked = that.groupIds.$contains(group.id)
return group
})
that.isLoaded = true
})
},
data: function () {
var groupIds = this.vGroupIds
if (groupIds == null) {
groupIds = []
}
return {
groups: [],
groupIds: groupIds,
isLoaded: false,
allGroups: groupIds.length == 0
}
},
methods: {
check: function (group) {
group.isChecked = !group.isChecked
this.groupIds = []
let that = this
this.groups.forEach(function (v) {
if (v.isChecked) {
that.groupIds.push(v.id)
}
})
this.change()
},
change: function () {
let that = this
let groups = []
this.groupIds.forEach(function (groupId) {
let group = that.groups.$find(function (k, v) {
return v.id == groupId
})
if (group == null) {
return
}
groups.push({
id: group.id,
name: group.name
})
})
this.$emit("change", groups)
}
},
watch: {
allGroups: function (b) {
if (b) {
this.groupIds = []
this.groups.forEach(function (v) {
v.isChecked = false
})
}
this.change()
}
},
template: ``
})
Vue.component("finance-user-selector", {
props: ["v-user-id"],
data: function () {
return {}
},
methods: {
change: function (userId) {
this.$emit("change", userId)
}
},
template: `
`
})
// 节点登录推荐端口
Vue.component("node-login-suggest-ports", {
data: function () {
return {
ports: [],
availablePorts: [],
autoSelected: false,
isLoading: false
}
},
methods: {
reload: function (host) {
let that = this
this.autoSelected = false
this.isLoading = true
Tea.action("/clusters/cluster/suggestLoginPorts")
.params({
host: host
})
.success(function (resp) {
if (resp.data.availablePorts != null) {
that.availablePorts = resp.data.availablePorts
if (that.availablePorts.length > 0) {
that.autoSelectPort(that.availablePorts[0])
that.autoSelected = true
}
}
if (resp.data.ports != null) {
that.ports = resp.data.ports
if (that.ports.length > 0 && !that.autoSelected) {
that.autoSelectPort(that.ports[0])
that.autoSelected = true
}
}
})
.done(function () {
that.isLoading = false
})
.post()
},
selectPort: function (port) {
this.$emit("select", port)
},
autoSelectPort: function (port) {
this.$emit("auto-select", port)
}
},
template: `
正在检查端口...
可能端口:{{port}}
常用端口:{{port}}
常用端口有22等。
(可以点击要使用的端口)
`
})
Vue.component("node-group-selector", {
props: ["v-cluster-id", "v-group"],
data: function () {
return {
selectedGroup: this.vGroup
}
},
methods: {
selectGroup: function () {
let that = this
teaweb.popup("/clusters/cluster/groups/selectPopup?clusterId=" + this.vClusterId, {
callback: function (resp) {
that.selectedGroup = resp.data.group
}
})
},
addGroup: function () {
let that = this
teaweb.popup("/clusters/cluster/groups/createPopup?clusterId=" + this.vClusterId, {
callback: function (resp) {
that.selectedGroup = resp.data.group
}
})
},
removeGroup: function () {
this.selectedGroup = null
}
},
template: ``
})
// 节点IP地址管理(标签形式)
Vue.component("node-ip-addresses-box", {
props: ["v-ip-addresses", "role"],
data: function () {
return {
ipAddresses: (this.vIpAddresses == null) ? [] : this.vIpAddresses,
supportThresholds: this.role != "ns"
}
},
methods: {
// 添加IP地址
addIPAddress: function () {
window.UPDATING_NODE_IP_ADDRESS = null
let that = this;
teaweb.popup("/nodes/ipAddresses/createPopup?supportThresholds=" + (this.supportThresholds ? 1 : 0), {
callback: function (resp) {
that.ipAddresses.push(resp.data.ipAddress);
},
height: "24em",
width: "44em"
})
},
// 修改地址
updateIPAddress: function (index, address) {
window.UPDATING_NODE_IP_ADDRESS = address
let that = this;
teaweb.popup("/nodes/ipAddresses/updatePopup?supportThresholds=" + (this.supportThresholds ? 1 : 0), {
callback: function (resp) {
Vue.set(that.ipAddresses, index, resp.data.ipAddress);
},
height: "24em",
width: "44em"
})
},
// 删除IP地址
removeIPAddress: function (index) {
this.ipAddresses.$remove(index);
},
// 判断是否为IPv6
isIPv6: function (ip) {
return ip.indexOf(":") > -1
}
},
template: `
[IPv6] {{address.ip}}
({{address.name}},不可访问 )
(不可访问)
[off]
[down]
[{{address.thresholds.length}}个阈值]
+
`
})
// 节点IP阈值
Vue.component("node-ip-address-thresholds-view", {
props: ["v-thresholds"],
data: function () {
let thresholds = this.vThresholds
if (thresholds == null) {
thresholds = []
} else {
thresholds.forEach(function (v) {
if (v.items == null) {
v.items = []
}
if (v.actions == null) {
v.actions = []
}
})
}
return {
thresholds: thresholds,
allItems: window.IP_ADDR_THRESHOLD_ITEMS,
allOperators: [
{
"name": "小于等于",
"code": "lte"
},
{
"name": "大于",
"code": "gt"
},
{
"name": "不等于",
"code": "neq"
},
{
"name": "小于",
"code": "lt"
},
{
"name": "大于等于",
"code": "gte"
}
],
allActions: window.IP_ADDR_THRESHOLD_ACTIONS
}
},
methods: {
itemName: function (item) {
let result = ""
this.allItems.forEach(function (v) {
if (v.code == item) {
result = v.name
}
})
return result
},
itemUnitName: function (itemCode) {
let result = ""
this.allItems.forEach(function (v) {
if (v.code == itemCode) {
result = v.unit
}
})
return result
},
itemDurationUnitName: function (unit) {
switch (unit) {
case "minute":
return "分钟"
case "second":
return "秒"
case "hour":
return "小时"
case "day":
return "天"
}
return unit
},
itemOperatorName: function (operator) {
let result = ""
this.allOperators.forEach(function (v) {
if (v.code == operator) {
result = v.name
}
})
return result
},
actionName: function (actionCode) {
let result = ""
this.allActions.forEach(function (v) {
if (v.code == actionCode) {
result = v.name
}
})
return result
}
},
template: `
[{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}]
{{itemName(item.item)}}
成功
失败
[{{group.name}} ]
[{{itemOperatorName(item.operator)}}] {{item.value}}{{itemUnitName(item.item)}}
AND
->
{{actionName(action.action)}}
到{{action.options.ips.join(", ")}}
({{action.options.url}})
AND
`
})
// 节点IP阈值
Vue.component("node-ip-address-thresholds-box", {
props: ["v-thresholds"],
data: function () {
let thresholds = this.vThresholds
if (thresholds == null) {
thresholds = []
} else {
thresholds.forEach(function (v) {
if (v.items == null) {
v.items = []
}
if (v.actions == null) {
v.actions = []
}
})
}
return {
editingIndex: -1,
thresholds: thresholds,
addingThreshold: {
items: [],
actions: []
},
isAdding: false,
isAddingItem: false,
isAddingAction: false,
itemCode: "nodeAvgRequests",
itemReportGroups: [],
itemOperator: "lte",
itemValue: "",
itemDuration: "5",
allItems: window.IP_ADDR_THRESHOLD_ITEMS,
allOperators: [
{
"name": "小于等于",
"code": "lte"
},
{
"name": "大于",
"code": "gt"
},
{
"name": "不等于",
"code": "neq"
},
{
"name": "小于",
"code": "lt"
},
{
"name": "大于等于",
"code": "gte"
}
],
allActions: window.IP_ADDR_THRESHOLD_ACTIONS,
actionCode: "up",
actionBackupIPs: "",
actionWebHookURL: ""
}
},
methods: {
add: function () {
this.isAdding = !this.isAdding
},
cancel: function () {
this.isAdding = false
this.editingIndex = -1
this.addingThreshold = {
items: [],
actions: []
}
},
confirm: function () {
if (this.addingThreshold.items.length == 0) {
teaweb.warn("需要至少添加一个阈值")
return
}
if (this.addingThreshold.actions.length == 0) {
teaweb.warn("需要至少添加一个动作")
return
}
if (this.editingIndex >= 0) {
this.thresholds[this.editingIndex].items = this.addingThreshold.items
this.thresholds[this.editingIndex].actions = this.addingThreshold.actions
} else {
this.thresholds.push({
items: this.addingThreshold.items,
actions: this.addingThreshold.actions
})
}
// 还原
this.cancel()
},
remove: function (index) {
this.cancel()
this.thresholds.$remove(index)
},
update: function (index) {
this.editingIndex = index
this.addingThreshold = {
items: this.thresholds[index].items.$copy(),
actions: this.thresholds[index].actions.$copy()
}
this.isAdding = true
},
addItem: function () {
this.isAddingItem = !this.isAddingItem
let that = this
setTimeout(function () {
that.$refs.itemValue.focus()
}, 100)
},
cancelItem: function () {
this.isAddingItem = false
this.itemCode = "nodeAvgRequests"
this.itmeOperator = "lte"
this.itemValue = ""
this.itemDuration = "5"
this.itemReportGroups = []
},
confirmItem: function () {
// 特殊阈值快速添加
if (["nodeHealthCheck"].$contains(this.itemCode)) {
if (this.itemValue.toString().length == 0) {
teaweb.warn("请选择检查结果")
return
}
let value = parseInt(this.itemValue)
if (isNaN(value)) {
value = 0
} else if (value < 0) {
value = 0
} else if (value > 1) {
value = 1
}
// 添加
this.addingThreshold.items.push({
item: this.itemCode,
operator: this.itemOperator,
value: value,
duration: 0,
durationUnit: "minute",
options: {}
})
this.cancelItem()
return
}
if (this.itemDuration.length == 0) {
let that = this
teaweb.warn("请输入统计周期", function () {
that.$refs.itemDuration.focus()
})
return
}
let itemDuration = parseInt(this.itemDuration)
if (isNaN(itemDuration) || itemDuration <= 0) {
teaweb.warn("请输入正确的统计周期", function () {
that.$refs.itemDuration.focus()
})
return
}
if (this.itemValue.length == 0) {
let that = this
teaweb.warn("请输入对比值", function () {
that.$refs.itemValue.focus()
})
return
}
let itemValue = parseFloat(this.itemValue)
if (isNaN(itemValue)) {
teaweb.warn("请输入正确的对比值", function () {
that.$refs.itemValue.focus()
})
return
}
let options = {}
switch (this.itemCode) {
case "connectivity": // 连通性校验
if (itemValue > 100) {
let that = this
teaweb.warn("连通性对比值不能超过100", function () {
that.$refs.itemValue.focus()
})
return
}
options["groups"] = this.itemReportGroups
break
}
// 添加
this.addingThreshold.items.push({
item: this.itemCode,
operator: this.itemOperator,
value: itemValue,
duration: itemDuration,
durationUnit: "minute",
options: options
})
// 还原
this.cancelItem()
},
removeItem: function (index) {
this.cancelItem()
this.addingThreshold.items.$remove(index)
},
changeReportGroups: function (groups) {
this.itemReportGroups = groups
},
itemName: function (item) {
let result = ""
this.allItems.forEach(function (v) {
if (v.code == item) {
result = v.name
}
})
return result
},
itemUnitName: function (itemCode) {
let result = ""
this.allItems.forEach(function (v) {
if (v.code == itemCode) {
result = v.unit
}
})
return result
},
itemDurationUnitName: function (unit) {
switch (unit) {
case "minute":
return "分钟"
case "second":
return "秒"
case "hour":
return "小时"
case "day":
return "天"
}
return unit
},
itemOperatorName: function (operator) {
let result = ""
this.allOperators.forEach(function (v) {
if (v.code == operator) {
result = v.name
}
})
return result
},
addAction: function () {
this.isAddingAction = !this.isAddingAction
},
cancelAction: function () {
this.isAddingAction = false
this.actionCode = "up"
this.actionBackupIPs = ""
this.actionWebHookURL = ""
},
confirmAction: function () {
this.doConfirmAction(false)
},
doConfirmAction: function (validated, options) {
// 是否已存在
let exists = false
let that = this
this.addingThreshold.actions.forEach(function (v) {
if (v.action == that.actionCode) {
exists = true
}
})
if (exists) {
teaweb.warn("此动作已经添加过了,无需重复添加")
return
}
if (options == null) {
options = {}
}
switch (this.actionCode) {
case "switch":
if (!validated) {
Tea.action("/ui/validateIPs")
.params({
"ips": this.actionBackupIPs
})
.success(function (resp) {
if (resp.data.ips.length == 0) {
teaweb.warn("请输入备用IP", function () {
that.$refs.actionBackupIPs.focus()
})
return
}
options["ips"] = resp.data.ips
that.doConfirmAction(true, options)
})
.fail(function (resp) {
teaweb.warn("输入的IP '" + resp.data.failIP + "' 格式不正确,请改正后提交", function () {
that.$refs.actionBackupIPs.focus()
})
})
.post()
return
}
break
case "webHook":
if (this.actionWebHookURL.length == 0) {
teaweb.warn("请输入WebHook URL", function () {
that.$refs.webHookURL.focus()
})
return
}
if (!this.actionWebHookURL.match(/^(http|https):\/\//i)) {
teaweb.warn("URL开头必须是http://或者https://", function () {
that.$refs.webHookURL.focus()
})
return
}
options["url"] = this.actionWebHookURL
}
this.addingThreshold.actions.push({
action: this.actionCode,
options: options
})
// 还原
this.cancelAction()
},
removeAction: function (index) {
this.cancelAction()
this.addingThreshold.actions.$remove(index)
},
actionName: function (actionCode) {
let result = ""
this.allActions.forEach(function (v) {
if (v.code == actionCode) {
result = v.name
}
})
return result
}
},
template: `
[{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}]
{{itemName(item.item)}}
成功
失败
[{{group.name}} ]
[{{itemOperatorName(item.operator)}}] {{item.value}}{{itemUnitName(item.item)}}
AND
->
{{actionName(action.action)}}
到{{action.options.ips.join(", ")}}
({{action.options.url}})
AND
阈值
动作
[{{item.duration}}{{itemDurationUnitName(item.durationUnit)}}]
{{itemName(item.item)}}
成功
失败
[{{group.name}} ]
[{{itemOperatorName(item.operator)}}] {{item.value}}{{itemUnitName(item.item)}}
+
{{actionName(action.action)}}
到{{action.options.ips.join(", ")}}
({{action.options.url}})
+
+
`
})
Vue.component("node-region-selector", {
props: ["v-region"],
data: function () {
return {
selectedRegion: this.vRegion
}
},
methods: {
selectRegion: function () {
let that = this
teaweb.popup("/clusters/regions/selectPopup?clusterId=" + this.vClusterId, {
callback: function (resp) {
that.selectedRegion = resp.data.region
}
})
},
addRegion: function () {
let that = this
teaweb.popup("/clusters/regions/createPopup?clusterId=" + this.vClusterId, {
callback: function (resp) {
that.selectedRegion = resp.data.region
}
})
},
removeRegion: function () {
this.selectedRegion = null
}
},
template: ``
})
Vue.component("node-combo-box", {
props: ["v-cluster-id", "v-node-id"],
data: function () {
let that = this
Tea.action("/clusters/nodeOptions")
.params({
clusterId: this.vClusterId
})
.post()
.success(function (resp) {
that.nodes = resp.data.nodes
})
return {
nodes: []
}
},
template: `
`
})
// 节点级别选择器
Vue.component("node-level-selector", {
props: ["v-node-level"],
data: function () {
let levelCode = this.vNodeLevel
if (levelCode == null || levelCode < 1) {
levelCode = 1
}
return {
levels: [
{
name: "边缘节点",
code: 1,
description: "普通的边缘节点。"
},
{
name: "L2节点",
code: 2,
description: "特殊的边缘节点,同时负责同组上一级节点的回源。"
}
],
levelCode: levelCode
}
},
watch: {
levelCode: function (code) {
this.$emit("change", code)
}
},
template: `
{{level.name}}
`
})
Vue.component("dns-route-selector", {
props: ["v-all-routes", "v-routes"],
data: function () {
let routes = this.vRoutes
if (routes == null) {
routes = []
}
routes.$sort(function (v1, v2) {
if (v1.domainId == v2.domainId) {
return v1.code < v2.code
}
return (v1.domainId < v2.domainId) ? 1 : -1
})
return {
routes: routes,
routeCodes: routes.$map(function (k, v) {
return v.code + "@" + v.domainId
}),
isAdding: false,
routeCode: "",
keyword: "",
searchingRoutes: this.vAllRoutes.$copy()
}
},
methods: {
add: function () {
this.isAdding = true
this.keyword = ""
this.routeCode = ""
let that = this
setTimeout(function () {
that.$refs.keywordRef.focus()
}, 200)
},
cancel: function () {
this.isAdding = false
},
confirm: function () {
if (this.routeCode.length == 0) {
return
}
if (this.routeCodes.$contains(this.routeCode)) {
teaweb.warn("已经添加过此线路,不能重复添加")
return
}
let that = this
let route = this.vAllRoutes.$find(function (k, v) {
return v.code + "@" + v.domainId == that.routeCode
})
if (route == null) {
return
}
this.routeCodes.push(this.routeCode)
this.routes.push(route)
this.routes.$sort(function (v1, v2) {
if (v1.domainId == v2.domainId) {
return v1.code < v2.code
}
return (v1.domainId < v2.domainId) ? 1 : -1
})
this.routeCode = ""
this.isAdding = false
},
remove: function (route) {
this.routeCodes.$removeValue(route.code + "@" + route.domainId)
this.routes.$removeIf(function (k, v) {
return v.code + "@" + v.domainId == route.code + "@" + route.domainId
})
}
},
watch: {
keyword: function (keyword) {
if (keyword.length == 0) {
this.searchingRoutes = this.vAllRoutes.$copy()
this.routeCode = ""
return
}
this.searchingRoutes = this.vAllRoutes.filter(function (route) {
return teaweb.match(route.name, keyword) || teaweb.match(route.domainName, keyword)
})
if (this.searchingRoutes.length > 0) {
this.routeCode = this.searchingRoutes[0].code + "@" + this.searchingRoutes[0].domainId
} else {
this.routeCode = ""
}
}
},
template: `
{{route.name}} ({{route.domainName}})
+
[请选择]
{{route.name}}({{route.domainName}})
确定
`
})
Vue.component("dns-domain-selector", {
props: ["v-domain-id", "v-domain-name"],
data: function () {
let domainId = this.vDomainId
if (domainId == null) {
domainId = 0
}
let domainName = this.vDomainName
if (domainName == null) {
domainName = ""
}
return {
domainId: domainId,
domainName: domainName
}
},
methods: {
select: function () {
let that = this
teaweb.popup("/dns/domains/selectPopup", {
callback: function (resp) {
that.domainId = resp.data.domainId
that.domainName = resp.data.domainName
that.change()
}
})
},
remove: function() {
this.domainId = 0
this.domainName = ""
this.change()
},
update: function () {
let that = this
teaweb.popup("/dns/domains/selectPopup?domainId=" + this.domainId, {
callback: function (resp) {
that.domainId = resp.data.domainId
that.domainName = resp.data.domainName
that.change()
}
})
},
change: function () {
this.$emit("change", {
id: this.domainId,
name: this.domainName
})
}
},
template: ``
})
Vue.component("dns-resolver-config-box", {
props:["v-dns-resolver-config"],
data: function () {
let config = this.vDnsResolverConfig
if (config == null) {
config = {
type: "default"
}
}
return {
config: config,
types: [
{
name: "默认",
code: "default"
},
{
name: "CGO",
code: "cgo"
},
{
name: "Go原生",
code: "goNative"
},
]
}
},
template: ``
})
Vue.component("grant-selector", {
props: ["v-grant", "v-node-cluster-id", "v-ns-cluster-id"],
data: function () {
return {
grantId: (this.vGrant == null) ? 0 : this.vGrant.id,
grant: this.vGrant,
nodeClusterId: (this.vNodeClusterId != null) ? this.vNodeClusterId : 0,
nsClusterId: (this.vNsClusterId != null) ? this.vNsClusterId : 0
}
},
methods: {
// 选择授权
select: function () {
let that = this;
teaweb.popup("/clusters/grants/selectPopup?nodeClusterId=" + this.nodeClusterId + "&nsClusterId=" + this.nsClusterId, {
callback: (resp) => {
that.grantId = resp.data.grant.id;
if (that.grantId > 0) {
that.grant = resp.data.grant;
}
that.notifyUpdate()
},
height: "26em"
})
},
// 创建授权
create: function () {
let that = this
teaweb.popup("/clusters/grants/createPopup", {
height: "26em",
callback: (resp) => {
that.grantId = resp.data.grant.id;
if (that.grantId > 0) {
that.grant = resp.data.grant;
}
that.notifyUpdate()
}
})
},
// 修改授权
update: function () {
if (this.grant == null) {
window.location.reload();
return;
}
let that = this
teaweb.popup("/clusters/grants/updatePopup?grantId=" + this.grant.id, {
height: "26em",
callback: (resp) => {
that.grant = resp.data.grant
that.notifyUpdate()
}
})
},
// 删除已选择授权
remove: function () {
this.grant = null
this.grantId = 0
this.notifyUpdate()
},
notifyUpdate: function () {
this.$emit("change", this.grant)
}
},
template: `
{{grant.name}}
({{grant.methodName}}) ({{grant.username}})
`
})
window.REQUEST_COND_COMPONENTS = [{"type":"url-extension","name":"URL扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-extension","paramsTitle":"扩展名列表","isRequest":true,"caseInsensitive":false},{"type":"url-eq-index","name":"首页","description":"检查URL路径是为\"/\"","component":"http-cond-url-eq-index","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":false},{"type":"url-prefix","name":"URL前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-prefix","paramsTitle":"URL前缀","isRequest":true,"caseInsensitive":true},{"type":"url-eq","name":"URL完整路径","description":"检查URL中的文件路径是否一致","component":"http-cond-url-eq","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":true},{"type":"url-regexp","name":"URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致","component":"http-cond-url-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"user-agent-regexp","name":"User-Agent正则匹配","description":"使用正则表达式检查User-Agent中是否含有某些浏览器和系统标识","component":"http-cond-user-agent-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"params","name":"参数匹配","description":"根据参数值进行匹配","component":"http-cond-params","paramsTitle":"参数配置","isRequest":true,"caseInsensitive":false},{"type":"url-not-extension","name":"排除:URL扩展名","description":"根据URL中的文件路径扩展名进行过滤","component":"http-cond-url-not-extension","paramsTitle":"扩展名列表","isRequest":true,"caseInsensitive":false},{"type":"url-not-prefix","name":"排除:URL前缀","description":"根据URL中的文件路径前缀进行过滤","component":"http-cond-url-not-prefix","paramsTitle":"URL前缀","isRequest":true,"caseInsensitive":true},{"type":"url-not-eq","name":"排除:URL完整路径","description":"检查URL中的文件路径是否一致","component":"http-cond-url-not-eq","paramsTitle":"URL完整路径","isRequest":true,"caseInsensitive":true},{"type":"url-not-regexp","name":"排除:URL正则匹配","description":"使用正则表达式检查URL中的文件路径是否一致,如果一致,则不匹配","component":"http-cond-url-not-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"user-agent-not-regexp","name":"排除:User-Agent正则匹配","description":"使用正则表达式检查User-Agent中是否含有某些浏览器和系统标识,如果含有,则不匹配","component":"http-cond-user-agent-not-regexp","paramsTitle":"正则表达式","isRequest":true,"caseInsensitive":true},{"type":"mime-type","name":"内容MimeType","description":"根据服务器返回的内容的MimeType进行过滤。注意:当用于缓存条件时,此条件需要结合别的请求条件使用。","component":"http-cond-mime-type","paramsTitle":"MimeType列表","isRequest":false,"caseInsensitive":false}]
window.REQUEST_COND_OPERATORS = [{"description":"判断是否正则表达式匹配","name":"正则表达式匹配","op":"regexp"},{"description":"判断是否正则表达式不匹配","name":"正则表达式不匹配","op":"not regexp"},{"description":"使用字符串对比参数值是否相等于某个值","name":"字符串等于","op":"eq"},{"description":"参数值包含某个前缀","name":"字符串前缀","op":"prefix"},{"description":"参数值包含某个后缀","name":"字符串后缀","op":"suffix"},{"description":"参数值包含另外一个字符串","name":"字符串包含","op":"contains"},{"description":"参数值不包含另外一个字符串","name":"字符串不包含","op":"not contains"},{"description":"使用字符串对比参数值是否不相等于某个值","name":"字符串不等于","op":"not"},{"description":"判断参数值在某个列表中","name":"在列表中","op":"in"},{"description":"判断参数值不在某个列表中","name":"不在列表中","op":"not in"},{"description":"判断小写的扩展名(不带点)在某个列表中","name":"扩展名","op":"file ext"},{"description":"判断MimeType在某个列表中,支持类似于image/*的语法","name":"MimeType","op":"mime type"},{"description":"判断版本号在某个范围内,格式为version1,version2","name":"版本号范围","op":"version range"},{"description":"将参数转换为整数数字后进行对比","name":"整数等于","op":"eq int"},{"description":"将参数转换为可以有小数的浮点数字进行对比","name":"浮点数等于","op":"eq float"},{"description":"将参数转换为数字进行对比","name":"数字大于","op":"gt"},{"description":"将参数转换为数字进行对比","name":"数字大于等于","op":"gte"},{"description":"将参数转换为数字进行对比","name":"数字小于","op":"lt"},{"description":"将参数转换为数字进行对比","name":"数字小于等于","op":"lte"},{"description":"对整数参数值取模,除数为10,对比值为余数","name":"整数取模10","op":"mod 10"},{"description":"对整数参数值取模,除数为100,对比值为余数","name":"整数取模100","op":"mod 100"},{"description":"对整数参数值取模,对比值格式为:除数,余数,比如10,1","name":"整数取模","op":"mod"},{"description":"将参数转换为IP进行对比","name":"IP等于","op":"eq ip"},{"description":"将参数转换为IP进行对比","name":"IP大于","op":"gt ip"},{"description":"将参数转换为IP进行对比","name":"IP大于等于","op":"gte ip"},{"description":"将参数转换为IP进行对比","name":"IP小于","op":"lt ip"},{"description":"将参数转换为IP进行对比","name":"IP小于等于","op":"lte ip"},{"description":"IP在某个范围之内,范围格式可以是英文逗号分隔的ip1,ip2,或者CIDR格式的ip/bits","name":"IP范围","op":"ip range"},{"description":"对IP参数值取模,除数为10,对比值为余数","name":"IP取模10","op":"ip mod 10"},{"description":"对IP参数值取模,除数为100,对比值为余数","name":"IP取模100","op":"ip mod 100"},{"description":"对IP参数值取模,对比值格式为:除数,余数,比如10,1","name":"IP取模","op":"ip mod"},{"description":"判断参数值解析后的文件是否存在","name":"文件存在","op":"file exist"},{"description":"判断参数值解析后的文件是否不存在","name":"文件不存在","op":"file not exist"}]
window.REQUEST_VARIABLES = [{"code":"${edgeVersion}","description":"","name":"边缘节点版本"},{"code":"${remoteAddr}","description":"会依次根据X-Forwarded-For、X-Real-IP、RemoteAddr获取,适合前端有别的反向代理服务时使用,存在伪造的风险","name":"客户端地址(IP)"},{"code":"${rawRemoteAddr}","description":"返回直接连接服务的客户端原始IP地址","name":"客户端地址(IP)"},{"code":"${remotePort}","description":"","name":"客户端端口"},{"code":"${remoteUser}","description":"","name":"客户端用户名"},{"code":"${requestURI}","description":"比如/hello?name=lily","name":"请求URI"},{"code":"${requestPath}","description":"比如/hello","name":"请求路径(不包括参数)"},{"code":"${requestURL}","description":"比如https://example.com/hello?name=lily","name":"完整的请求URL"},{"code":"${requestLength}","description":"","name":"请求内容长度"},{"code":"${requestMethod}","description":"比如GET、POST","name":"请求方法"},{"code":"${requestFilename}","description":"","name":"请求文件路径"},{"code":"${scheme}","description":"","name":"请求协议,http或https"},{"code":"${proto}","description:":"类似于HTTP/1.0","name":"包含版本的HTTP请求协议"},{"code":"${timeISO8601}","description":"比如2018-07-16T23:52:24.839+08:00","name":"ISO 8601格式的时间"},{"code":"${timeLocal}","description":"比如17/Jul/2018:09:52:24 +0800","name":"本地时间"},{"code":"${msec}","description":"比如1531756823.054","name":"带有毫秒的时间"},{"code":"${timestamp}","description":"","name":"unix时间戳,单位为秒"},{"code":"${host}","description":"","name":"主机名"},{"code":"${serverName}","description":"","name":"接收请求的服务器名"},{"code":"${serverPort}","description":"","name":"接收请求的服务器端口"},{"code":"${referer}","description":"","name":"请求来源URL"},{"code":"${referer.host}","description":"","name":"请求来源URL域名"},{"code":"${userAgent}","description":"","name":"客户端信息"},{"code":"${contentType}","description":"","name":"请求头部的Content-Type"},{"code":"${cookies}","description":"","name":"所有cookie组合字符串"},{"code":"${cookie.NAME}","description":"","name":"单个cookie值"},{"code":"${isArgs}","description":"如果URL有参数,则值为`?`;否则,则值为空","name":"问号(?)标记"},{"code":"${args}","description":"","name":"所有参数组合字符串"},{"code":"${arg.NAME}","description":"","name":"单个参数值"},{"code":"${headers}","description":"","name":"所有Header信息组合字符串"},{"code":"${header.NAME}","description":"","name":"单个Header值"},{"code":"${geo.country.name}","description":"","name":"国家/地区名称"},{"code":"${geo.country.id}","description":"","name":"国家/地区ID"},{"code":"${geo.province.name}","description":"目前只包含中国省份","name":"省份名称"},{"code":"${geo.province.id}","description":"目前只包含中国省份","name":"省份ID"},{"code":"${geo.city.name}","description":"目前只包含中国城市","name":"城市名称"},{"code":"${geo.city.id}","description":"目前只包含中国城市","name":"城市名称"},{"code":"${isp.name}","description":"","name":"ISP服务商名称"},{"code":"${isp.id}","description":"","name":"ISP服务商ID"},{"code":"${browser.os.name}","description":"客户端所在操作系统名称","name":"操作系统名称"},{"code":"${browser.os.version}","description":"客户端所在操作系统版本","name":"操作系统版本"},{"code":"${browser.name}","description":"客户端浏览器名称","name":"浏览器名称"},{"code":"${browser.version}","description":"客户端浏览器版本","name":"浏览器版本"},{"code":"${browser.isMobile}","description":"如果客户端是手机,则值为1,否则为0","name":"手机标识"}]
window.METRIC_HTTP_KEYS = [{"name":"客户端地址(IP)","code":"${remoteAddr}","description":"会依次根据X-Forwarded-For、X-Real-IP、RemoteAddr获取,适用于前端可能有别的反向代理的情形,存在被伪造的可能","icon":""},{"name":"直接客户端地址(IP)","code":"${rawRemoteAddr}","description":"返回直接连接服务的客户端原始IP地址","icon":""},{"name":"客户端用户名","code":"${remoteUser}","description":"通过基本认证填入的用户名","icon":""},{"name":"请求URI","code":"${requestURI}","description":"包含参数,比如/hello?name=lily","icon":""},{"name":"请求路径","code":"${requestPath}","description":"不包含参数,比如/hello","icon":""},{"name":"完整URL","code":"${requestURL}","description":"比如https://example.com/hello?name=lily","icon":""},{"name":"请求方法","code":"${requestMethod}","description":"比如GET、POST等","icon":""},{"name":"请求协议Scheme","code":"${scheme}","description":"http或https","icon":""},{"name":"文件扩展名","code":"${requestPathExtension}","description":"请求路径中的文件扩展名,包括点符号,比如.html、.png","icon":""},{"name":"主机名","code":"${host}","description":"通常是请求的域名","icon":""},{"name":"请求协议Proto","code":"${proto}","description":"包含版本的HTTP请求协议,类似于HTTP/1.0","icon":""},{"name":"HTTP协议","code":"${proto}","description":"包含版本的HTTP请求协议,类似于HTTP/1.0","icon":""},{"name":"URL参数值","code":"${arg.NAME}","description":"单个URL参数值","icon":""},{"name":"请求来源URL","code":"${referer}","description":"请求来源Referer URL","icon":""},{"name":"请求来源URL域名","code":"${referer.host}","description":"请求来源Referer URL域名","icon":""},{"name":"Header值","code":"${header.NAME}","description":"单个Header值,比如${header.User-Agent}","icon":""},{"name":"Cookie值","code":"${cookie.NAME}","description":"单个cookie值,比如${cookie.sid}","icon":""},{"name":"状态码","code":"${status}","description":"","icon":""},{"name":"响应的Content-Type值","code":"${response.contentType}","description":"","icon":""}]
window.IP_ADDR_THRESHOLD_ITEMS = [{"code":"nodeAvgRequests","description":"当前节点在单位时间内接收到的平均请求数。","name":"节点平均请求数","unit":"个"},{"code":"nodeAvgTrafficOut","description":"当前节点在单位时间内发送的下行流量。","name":"节点平均下行流量","unit":"M"},{"code":"nodeAvgTrafficIn","description":"当前节点在单位时间内接收的上行流量。","name":"节点平均上行流量","unit":"M"},{"code":"nodeHealthCheck","description":"当前节点健康检查结果。","name":"节点健康检查结果","unit":""},{"code":"connectivity","description":"通过区域监控得到的当前IP地址的连通性数值,取值在0和100之间。","name":"IP连通性","unit":"%"},{"code":"groupAvgRequests","description":"当前节点所在分组在单位时间内接收到的平均请求数。","name":"分组平均请求数","unit":"个"},{"code":"groupAvgTrafficOut","description":"当前节点所在分组在单位时间内发送的下行流量。","name":"分组平均下行流量","unit":"M"},{"code":"groupAvgTrafficIn","description":"当前节点所在分组在单位时间内接收的上行流量。","name":"分组平均上行流量","unit":"M"},{"code":"clusterAvgRequests","description":"当前节点所在集群在单位时间内接收到的平均请求数。","name":"集群平均请求数","unit":"个"},{"code":"clusterAvgTrafficOut","description":"当前节点所在集群在单位时间内发送的下行流量。","name":"集群平均下行流量","unit":"M"},{"code":"clusterAvgTrafficIn","description":"当前节点所在集群在单位时间内接收的上行流量。","name":"集群平均上行流量","unit":"M"}]
window.IP_ADDR_THRESHOLD_ACTIONS = [{"code":"up","description":"上线当前IP。","name":"上线"},{"code":"down","description":"下线当前IP。","name":"下线"},{"code":"notify","description":"发送已达到阈值通知。","name":"通知"},{"code":"switch","description":"在DNS中记录中将IP切换到指定的备用IP。","name":"切换"},{"code":"webHook","description":"调用外部的WebHook。","name":"WebHook"}]