mirror of
https://github.com/TeaOSLab/EdgeAdmin.git
synced 2025-11-18 06:40:25 +08:00
访问日志可以使用集群和节点搜索
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
"github.com/iwind/TeaGo/lists"
|
"github.com/iwind/TeaGo/lists"
|
||||||
|
"github.com/iwind/TeaGo/maps"
|
||||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -22,12 +23,14 @@ func (this *IndexAction) Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (this *IndexAction) RunGet(params struct {
|
func (this *IndexAction) RunGet(params struct {
|
||||||
Day string
|
ClusterId int64
|
||||||
Keyword string
|
NodeId int64
|
||||||
Ip string
|
Day string
|
||||||
Domain string
|
Keyword string
|
||||||
HasError int
|
Ip string
|
||||||
HasWAF int
|
Domain string
|
||||||
|
HasError int
|
||||||
|
HasWAF int
|
||||||
|
|
||||||
RequestId string
|
RequestId string
|
||||||
ServerId int64
|
ServerId int64
|
||||||
@@ -38,6 +41,8 @@ func (this *IndexAction) RunGet(params struct {
|
|||||||
params.Day = timeutil.Format("Y-m-d")
|
params.Day = timeutil.Format("Y-m-d")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.Data["clusterId"] = params.ClusterId
|
||||||
|
this.Data["nodeId"] = params.NodeId
|
||||||
this.Data["serverId"] = 0
|
this.Data["serverId"] = 0
|
||||||
this.Data["path"] = this.Request.URL.Path
|
this.Data["path"] = this.Request.URL.Path
|
||||||
this.Data["day"] = params.Day
|
this.Data["day"] = params.Day
|
||||||
@@ -66,6 +71,8 @@ func (this *IndexAction) RunGet(params struct {
|
|||||||
var before = time.Now()
|
var before = time.Now()
|
||||||
resp, err := this.RPC().HTTPAccessLogRPC().ListHTTPAccessLogs(this.AdminContext(), &pb.ListHTTPAccessLogsRequest{
|
resp, err := this.RPC().HTTPAccessLogRPC().ListHTTPAccessLogs(this.AdminContext(), &pb.ListHTTPAccessLogsRequest{
|
||||||
RequestId: params.RequestId,
|
RequestId: params.RequestId,
|
||||||
|
NodeClusterId: params.ClusterId,
|
||||||
|
NodeId: params.NodeId,
|
||||||
ServerId: params.ServerId,
|
ServerId: params.ServerId,
|
||||||
HasError: params.HasError > 0,
|
HasError: params.HasError > 0,
|
||||||
HasFirewallPolicy: params.HasWAF > 0,
|
HasFirewallPolicy: params.HasWAF > 0,
|
||||||
@@ -108,6 +115,8 @@ func (this *IndexAction) RunGet(params struct {
|
|||||||
this.Data["hasPrev"] = true
|
this.Data["hasPrev"] = true
|
||||||
prevResp, err := this.RPC().HTTPAccessLogRPC().ListHTTPAccessLogs(this.AdminContext(), &pb.ListHTTPAccessLogsRequest{
|
prevResp, err := this.RPC().HTTPAccessLogRPC().ListHTTPAccessLogs(this.AdminContext(), &pb.ListHTTPAccessLogsRequest{
|
||||||
RequestId: params.RequestId,
|
RequestId: params.RequestId,
|
||||||
|
NodeClusterId: params.ClusterId,
|
||||||
|
NodeId: params.NodeId,
|
||||||
ServerId: params.ServerId,
|
ServerId: params.ServerId,
|
||||||
HasError: params.HasError > 0,
|
HasError: params.HasError > 0,
|
||||||
HasFirewallPolicy: params.HasWAF > 0,
|
HasFirewallPolicy: params.HasWAF > 0,
|
||||||
@@ -144,5 +153,20 @@ func (this *IndexAction) RunGet(params struct {
|
|||||||
}
|
}
|
||||||
this.Data["regions"] = regionMap
|
this.Data["regions"] = regionMap
|
||||||
|
|
||||||
|
// 集群列表
|
||||||
|
var clusterMaps = []maps.Map{}
|
||||||
|
clusterResp, err := this.RPC().NodeClusterRPC().FindAllEnabledNodeClusters(this.AdminContext(), &pb.FindAllEnabledNodeClustersRequest{})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, cluster := range clusterResp.NodeClusters {
|
||||||
|
clusterMaps = append(clusterMaps, maps.Map{
|
||||||
|
"id": cluster.Id,
|
||||||
|
"name": cluster.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.Data["clusters"] = clusterMaps
|
||||||
|
|
||||||
this.Show()
|
this.Show()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ func init() {
|
|||||||
Prefix("/servers/logs").
|
Prefix("/servers/logs").
|
||||||
Get("", new(IndexAction)).
|
Get("", new(IndexAction)).
|
||||||
GetPost("/settings", new(SettingsAction)).
|
GetPost("/settings", new(SettingsAction)).
|
||||||
|
Post("/nodeOptions", new(NodeOptionsAction)).
|
||||||
EndAll()
|
EndAll()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
34
internal/web/actions/default/servers/logs/nodeOptions.go
Normal file
34
internal/web/actions/default/servers/logs/nodeOptions.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||||
|
|
||||||
|
package logs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||||
|
"github.com/iwind/TeaGo/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NodeOptionsAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *NodeOptionsAction) RunPost(params struct {
|
||||||
|
ClusterId int64
|
||||||
|
}) {
|
||||||
|
resp, err := this.RPC().NodeRPC().FindAllEnabledNodesWithNodeClusterId(this.AdminContext(), &pb.FindAllEnabledNodesWithNodeClusterIdRequest{NodeClusterId: params.ClusterId})
|
||||||
|
if err != nil {
|
||||||
|
this.ErrorPage(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodeMaps = []maps.Map{}
|
||||||
|
for _, node := range resp.Nodes {
|
||||||
|
nodeMaps = append(nodeMaps, maps.Map{
|
||||||
|
"id": node.Id,
|
||||||
|
"name": node.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.Data["nodes"] = nodeMaps
|
||||||
|
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
@@ -2257,7 +2257,7 @@ Vue.component("http-firewall-rule-label", {
|
|||||||
|
|
||||||
},
|
},
|
||||||
template: `<div>
|
template: `<div>
|
||||||
<div class="ui label tiny basic">
|
<div class="ui label tiny basic" style="line-height: 1.5">
|
||||||
{{rule.name}}[{{rule.param}}]
|
{{rule.name}}[{{rule.param}}]
|
||||||
|
|
||||||
<!-- cc2 -->
|
<!-- cc2 -->
|
||||||
@@ -2276,6 +2276,9 @@ Vue.component("http-firewall-rule-label", {
|
|||||||
{{rule.value}}
|
{{rule.value}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<!-- description -->
|
||||||
|
<span v-if="rule.description != null && rule.description.length > 0" class="grey small">({{rule.description}})</span>
|
||||||
|
|
||||||
<a href="" v-if="rule.err != null && rule.err.length > 0" @click.prevent="showErr(rule.err)" style="color: #db2828; opacity: 1; border-bottom: 1px #db2828 dashed; margin-left: 0.5em">规则错误</a>
|
<a href="" v-if="rule.err != null && rule.err.length > 0" @click.prevent="showErr(rule.err)" style="color: #db2828; opacity: 1; border-bottom: 1px #db2828 dashed; margin-left: 0.5em">规则错误</a>
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
@@ -7833,7 +7836,14 @@ Vue.component("http-remote-addr-config-box", {
|
|||||||
|
|
||||||
// 访问日志搜索框
|
// 访问日志搜索框
|
||||||
Vue.component("http-access-log-search-box", {
|
Vue.component("http-access-log-search-box", {
|
||||||
props: ["v-ip", "v-domain", "v-keyword"],
|
props: ["v-ip", "v-domain", "v-keyword", "v-cluster-id", "v-node-id", "v-clusters"],
|
||||||
|
mounted: function () {
|
||||||
|
if (this.vClusterId >0) {
|
||||||
|
this.changeCluster({
|
||||||
|
value: this.vClusterId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
let ip = this.vIp
|
let ip = this.vIp
|
||||||
if (ip == null) {
|
if (ip == null) {
|
||||||
@@ -7853,7 +7863,8 @@ Vue.component("http-access-log-search-box", {
|
|||||||
return {
|
return {
|
||||||
ip: ip,
|
ip: ip,
|
||||||
domain: domain,
|
domain: domain,
|
||||||
keyword: keyword
|
keyword: keyword,
|
||||||
|
nodes: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -7885,9 +7896,23 @@ Vue.component("http-access-log-search-box", {
|
|||||||
parent.submit()
|
parent.submit()
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
changeCluster: function (item) {
|
||||||
|
this.nodes = []
|
||||||
|
if (item != null) {
|
||||||
|
let that = this
|
||||||
|
Tea.action("/servers/logs/nodeOptions")
|
||||||
|
.params({
|
||||||
|
clusterId: item.value
|
||||||
|
})
|
||||||
|
.post()
|
||||||
|
.success(function (resp) {
|
||||||
|
that.nodes = resp.data.nodes
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `<div>
|
template: `<div style="z-index: 10">
|
||||||
<div class="margin"></div>
|
<div class="margin"></div>
|
||||||
<div class="ui fields inline">
|
<div class="ui fields inline">
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
@@ -7912,8 +7937,16 @@ Vue.component("http-access-log-search-box", {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<div class="ui fields inline" style="margin-top: 0.5em">
|
||||||
|
<div class="ui field" v-if="vClusters != null && vClusters.length > 0">
|
||||||
|
<combo-box title="集群" name="clusterId" placeholder="集群名称" :v-items="vClusters" :v-value="vClusterId" @change="changeCluster"></combo-box>
|
||||||
|
</div>
|
||||||
|
<div class="ui field" v-if="nodes.length > 0">
|
||||||
|
<combo-box title="节点" name="nodeId" placeholder="节点名称" :v-items="nodes" :v-value="vNodeId"></combo-box>
|
||||||
|
</div>
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<button class="ui button small" type="submit">查找</button>
|
<button class="ui button small" type="submit">搜索日志</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
@@ -8412,7 +8445,8 @@ Vue.component("http-firewall-block-options", {
|
|||||||
return {
|
return {
|
||||||
blockOptions: this.vBlockOptions,
|
blockOptions: this.vBlockOptions,
|
||||||
statusCode: this.vBlockOptions.statusCode,
|
statusCode: this.vBlockOptions.statusCode,
|
||||||
timeout: this.vBlockOptions.timeout
|
timeout: this.vBlockOptions.timeout,
|
||||||
|
isEditing: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -8433,9 +8467,15 @@ Vue.component("http-firewall-block-options", {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
edit: function () {
|
||||||
|
this.isEditing = !this.isEditing
|
||||||
|
}
|
||||||
|
},
|
||||||
template: `<div>
|
template: `<div>
|
||||||
<input type="hidden" name="blockOptionsJSON" :value="JSON.stringify(blockOptions)"/>
|
<input type="hidden" name="blockOptionsJSON" :value="JSON.stringify(blockOptions)"/>
|
||||||
<table class="ui table">
|
<a href="" @click.prevent="edit">状态码:{{statusCode}} / 提示内容:<span v-if="blockOptions.body != null && blockOptions.body.length > 0">[{{blockOptions.body.length}}字符]</span><span v-else class="disabled">[无]</span> / 超时时间:{{timeout}}秒 <i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i></a>
|
||||||
|
<table class="ui table" v-show="isEditing">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="title">状态码</td>
|
<td class="title">状态码</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -8520,6 +8560,9 @@ Vue.component("http-firewall-rules-box", {
|
|||||||
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span> <var>{{rule.operator}}</var> {{rule.value}}
|
<span v-if="rule.paramFilters != null && rule.paramFilters.length > 0" v-for="paramFilter in rule.paramFilters"> | {{paramFilter.code}}</span> <var>{{rule.operator}}</var> {{rule.value}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<!-- description -->
|
||||||
|
<span v-if="rule.description != null && rule.description.length > 0" class="grey small">({{rule.description}})</span>
|
||||||
|
|
||||||
<a href="" title="修改" @click.prevent="updateRule(index, rule)"><i class="icon pencil small"></i></a>
|
<a href="" title="修改" @click.prevent="updateRule(index, rule)"><i class="icon pencil small"></i></a>
|
||||||
<a href="" title="删除" @click.prevent="removeRule(index)"><i class="icon remove"></i></a>
|
<a href="" title="删除" @click.prevent="removeRule(index)"><i class="icon remove"></i></a>
|
||||||
</div>
|
</div>
|
||||||
@@ -9824,6 +9867,99 @@ Vue.component("traffic-limit-config-box", {
|
|||||||
</div>`
|
</div>`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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 < 3) {
|
||||||
|
count = 3
|
||||||
|
}
|
||||||
|
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: `<div>
|
||||||
|
<input type="hidden" name="synFloodJSON" :value="JSON.stringify(config)"/>
|
||||||
|
<a href="" @click.prevent="edit">
|
||||||
|
<span v-if="config.isOn">
|
||||||
|
已启用 / <span>空连接次数:{{config.minAttempts}}次/分钟</span> / 封禁时间:{{config.timeoutSeconds}}秒 <span v-if="config.ignoreLocal">/ 忽略局域网访问</span>
|
||||||
|
</span>
|
||||||
|
<span v-else>未启用</span>
|
||||||
|
<i class="icon angle" :class="{up: isEditing, down: !isEditing}"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<table class="ui table selectable" v-show="isEditing">
|
||||||
|
<tr>
|
||||||
|
<td class="title">是否启用</td>
|
||||||
|
<td>
|
||||||
|
<checkbox v-model="config.isOn"></checkbox>
|
||||||
|
<p class="comment">启用后,WAF将会尝试自动检测并阻止SYN Flood攻击。此功能需要节点已安装并启用Firewalld。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>空连接次数</td>
|
||||||
|
<td>
|
||||||
|
<div class="ui input right labeled">
|
||||||
|
<input type="text" v-model="minAttempts" style="width: 5em" maxlength="4"/>
|
||||||
|
<span class="ui label">次/分钟</span>
|
||||||
|
</div>
|
||||||
|
<p class="comment">超过此数字的"空连接"将被视为SYN Flood攻击,为了防止误判,此数值默认不小于3。</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>封禁时间</td>
|
||||||
|
<td>
|
||||||
|
<div class="ui input right labeled">
|
||||||
|
<input type="text" v-model="timeoutSeconds" style="width: 5em" maxlength="4"/>
|
||||||
|
<span class="ui label">秒</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>忽略局域网访问</td>
|
||||||
|
<td>
|
||||||
|
<checkbox v-model="config.ignoreLocal"></checkbox>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
|
|
||||||
// TODO 支持关键词搜索
|
// TODO 支持关键词搜索
|
||||||
// TODO 改成弹窗选择
|
// TODO 改成弹窗选择
|
||||||
Vue.component("admin-selector", {
|
Vue.component("admin-selector", {
|
||||||
@@ -10091,6 +10227,9 @@ Vue.component("ip-list-table", {
|
|||||||
<a :href="'/servers/components/waf/group?firewallPolicyId=' + item.sourcePolicy.id + '&type=inbound&groupId=' + item.sourceGroup.id" v-if="item.sourcePolicy.serverId == 0"><span class="small "><i class="icon shield"></i>{{item.sourcePolicy.name}} » {{item.sourceGroup.name}} » {{item.sourceSet.name}}</span></a>
|
<a :href="'/servers/components/waf/group?firewallPolicyId=' + item.sourcePolicy.id + '&type=inbound&groupId=' + item.sourceGroup.id" v-if="item.sourcePolicy.serverId == 0"><span class="small "><i class="icon shield"></i>{{item.sourcePolicy.name}} » {{item.sourceGroup.name}} » {{item.sourceSet.name}}</span></a>
|
||||||
<a :href="'/servers/server/settings/waf/group?serverId=' + item.sourcePolicy.serverId + '&firewallPolicyId=' + item.sourcePolicy.id + '&type=inbound&groupId=' + item.sourceGroup.id" v-if="item.sourcePolicy.serverId > 0"><span class="small "><i class="icon shield"></i> {{item.sourcePolicy.name}} » {{item.sourceGroup.name}} » {{item.sourceSet.name}}</span></a>
|
<a :href="'/servers/server/settings/waf/group?serverId=' + item.sourcePolicy.serverId + '&firewallPolicyId=' + item.sourcePolicy.id + '&type=inbound&groupId=' + item.sourceGroup.id" v-if="item.sourcePolicy.serverId > 0"><span class="small "><i class="icon shield"></i> {{item.sourcePolicy.name}} » {{item.sourceGroup.name}} » {{item.sourceSet.name}}</span></a>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="item.sourceNode != null && item.sourceNode.id > 0" style="margin-top: 0.4em">
|
||||||
|
<a :href="'/clusters/cluster/node?clusterId=' + item.sourceNode.clusterId + '&nodeId=' + item.sourceNode.id"><span class="small"><i class="icon cloud"></i>{{item.sourceNode.name}}</span></a>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="" @click.prevent="viewLogs(item.id)">日志</a>
|
<a href="" @click.prevent="viewLogs(item.id)">日志</a>
|
||||||
@@ -11468,6 +11607,143 @@ Vue.component("request-variables-describer", {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
Vue.component("combo-box", {
|
||||||
|
props: ["name", "title", "placeholder", "size", "v-items", "v-value"],
|
||||||
|
data: function () {
|
||||||
|
let items = this.vItems
|
||||||
|
if (items == null || !(items instanceof Array)) {
|
||||||
|
items = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动使用ID作为值
|
||||||
|
items.forEach(function (v) {
|
||||||
|
if (v.value == null) {
|
||||||
|
v.value = v.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前选中项
|
||||||
|
let selectedItem = null
|
||||||
|
if (this.vValue != null) {
|
||||||
|
let that = this
|
||||||
|
items.forEach(function (v) {
|
||||||
|
if (v.value == that.vValue) {
|
||||||
|
selectedItem = v
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allItems: items,
|
||||||
|
items: items.$copy(),
|
||||||
|
selectedItem: selectedItem,
|
||||||
|
keyword: "",
|
||||||
|
visible: false,
|
||||||
|
hideTimer: null,
|
||||||
|
hoverIndex: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
reset: function () {
|
||||||
|
this.selectedItem = null
|
||||||
|
this.change()
|
||||||
|
this.hoverIndex = 0
|
||||||
|
|
||||||
|
let that = this
|
||||||
|
setTimeout(function () {
|
||||||
|
if (that.$refs.searchBox) {
|
||||||
|
that.$refs.searchBox.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
changeKeyword: function () {
|
||||||
|
this.hoverIndex = 0
|
||||||
|
let keyword = this.keyword
|
||||||
|
if (keyword.length == 0) {
|
||||||
|
this.items = this.allItems.$copy()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.items = this.allItems.$copy().filter(function (v) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `<div style="display: inline">
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<div v-if="selectedItem == null">
|
||||||
|
<input type="text" v-model="keyword" :placeholder="placeholder" :size="size" style="width: 11em" @input="changeKeyword" @focus="show" @blur="hide" @keyup.enter="confirm()" @keypress.enter.prevent="1" ref="searchBox" @keyup.down="downItem" @keyup.up="upItem"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 当前选中 -->
|
||||||
|
<div v-if="selectedItem != null">
|
||||||
|
<input type="hidden" :name="name" :value="selectedItem.value"/>
|
||||||
|
<span class="ui label basic">{{title}}:{{selectedItem.name}}
|
||||||
|
<a href="" title="清除" @click.prevent="reset"><i class="icon remove small"></i></a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 菜单 -->
|
||||||
|
<div v-if="selectedItem == null && items.length > 0 && visible">
|
||||||
|
<div class="ui menu vertical small narrow-scrollbar" style="width: 11em; max-height: 17em; overflow-y: auto; position: absolute; border: rgba(129, 177, 210, 0.81) 1px solid; border-top: 0">
|
||||||
|
<a href="" v-for="(item, index) in items" ref="itemRef" class="item" :class="{active: index == hoverIndex, blue: index == hoverIndex}" @click.prevent="selectItem(item)" style="line-height: 1.4">{{item.name}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
|
|
||||||
Vue.component("time-duration-box", {
|
Vue.component("time-duration-box", {
|
||||||
props: ["v-name", "v-value", "v-count", "v-unit"],
|
props: ["v-name", "v-value", "v-count", "v-unit"],
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
|
|||||||
136
web/public/js/components/common/combo-box.js
Normal file
136
web/public/js/components/common/combo-box.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
Vue.component("combo-box", {
|
||||||
|
props: ["name", "title", "placeholder", "size", "v-items", "v-value"],
|
||||||
|
data: function () {
|
||||||
|
let items = this.vItems
|
||||||
|
if (items == null || !(items instanceof Array)) {
|
||||||
|
items = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动使用ID作为值
|
||||||
|
items.forEach(function (v) {
|
||||||
|
if (v.value == null) {
|
||||||
|
v.value = v.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前选中项
|
||||||
|
let selectedItem = null
|
||||||
|
if (this.vValue != null) {
|
||||||
|
let that = this
|
||||||
|
items.forEach(function (v) {
|
||||||
|
if (v.value == that.vValue) {
|
||||||
|
selectedItem = v
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allItems: items,
|
||||||
|
items: items.$copy(),
|
||||||
|
selectedItem: selectedItem,
|
||||||
|
keyword: "",
|
||||||
|
visible: false,
|
||||||
|
hideTimer: null,
|
||||||
|
hoverIndex: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
reset: function () {
|
||||||
|
this.selectedItem = null
|
||||||
|
this.change()
|
||||||
|
this.hoverIndex = 0
|
||||||
|
|
||||||
|
let that = this
|
||||||
|
setTimeout(function () {
|
||||||
|
if (that.$refs.searchBox) {
|
||||||
|
that.$refs.searchBox.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
changeKeyword: function () {
|
||||||
|
this.hoverIndex = 0
|
||||||
|
let keyword = this.keyword
|
||||||
|
if (keyword.length == 0) {
|
||||||
|
this.items = this.allItems.$copy()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.items = this.allItems.$copy().filter(function (v) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `<div style="display: inline">
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<div v-if="selectedItem == null">
|
||||||
|
<input type="text" v-model="keyword" :placeholder="placeholder" :size="size" style="width: 11em" @input="changeKeyword" @focus="show" @blur="hide" @keyup.enter="confirm()" @keypress.enter.prevent="1" ref="searchBox" @keyup.down="downItem" @keyup.up="upItem"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 当前选中 -->
|
||||||
|
<div v-if="selectedItem != null">
|
||||||
|
<input type="hidden" :name="name" :value="selectedItem.value"/>
|
||||||
|
<span class="ui label basic">{{title}}:{{selectedItem.name}}
|
||||||
|
<a href="" title="清除" @click.prevent="reset"><i class="icon remove small"></i></a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 菜单 -->
|
||||||
|
<div v-if="selectedItem == null && items.length > 0 && visible">
|
||||||
|
<div class="ui menu vertical small narrow-scrollbar" style="width: 11em; max-height: 17em; overflow-y: auto; position: absolute; border: rgba(129, 177, 210, 0.81) 1px solid; border-top: 0">
|
||||||
|
<a href="" v-for="(item, index) in items" ref="itemRef" class="item" :class="{active: index == hoverIndex, blue: index == hoverIndex}" @click.prevent="selectItem(item)" style="line-height: 1.4">{{item.name}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
// 访问日志搜索框
|
// 访问日志搜索框
|
||||||
Vue.component("http-access-log-search-box", {
|
Vue.component("http-access-log-search-box", {
|
||||||
props: ["v-ip", "v-domain", "v-keyword"],
|
props: ["v-ip", "v-domain", "v-keyword", "v-cluster-id", "v-node-id", "v-clusters"],
|
||||||
|
mounted: function () {
|
||||||
|
if (this.vClusterId >0) {
|
||||||
|
this.changeCluster({
|
||||||
|
value: this.vClusterId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
let ip = this.vIp
|
let ip = this.vIp
|
||||||
if (ip == null) {
|
if (ip == null) {
|
||||||
@@ -20,7 +27,8 @@ Vue.component("http-access-log-search-box", {
|
|||||||
return {
|
return {
|
||||||
ip: ip,
|
ip: ip,
|
||||||
domain: domain,
|
domain: domain,
|
||||||
keyword: keyword
|
keyword: keyword,
|
||||||
|
nodes: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -52,9 +60,23 @@ Vue.component("http-access-log-search-box", {
|
|||||||
parent.submit()
|
parent.submit()
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
changeCluster: function (item) {
|
||||||
|
this.nodes = []
|
||||||
|
if (item != null) {
|
||||||
|
let that = this
|
||||||
|
Tea.action("/servers/logs/nodeOptions")
|
||||||
|
.params({
|
||||||
|
clusterId: item.value
|
||||||
|
})
|
||||||
|
.post()
|
||||||
|
.success(function (resp) {
|
||||||
|
that.nodes = resp.data.nodes
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `<div>
|
template: `<div style="z-index: 10">
|
||||||
<div class="margin"></div>
|
<div class="margin"></div>
|
||||||
<div class="ui fields inline">
|
<div class="ui fields inline">
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
@@ -79,8 +101,16 @@ Vue.component("http-access-log-search-box", {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<div class="ui fields inline" style="margin-top: 0.5em">
|
||||||
|
<div class="ui field" v-if="vClusters != null && vClusters.length > 0">
|
||||||
|
<combo-box title="集群" name="clusterId" placeholder="集群名称" :v-items="vClusters" :v-value="vClusterId" @change="changeCluster"></combo-box>
|
||||||
|
</div>
|
||||||
|
<div class="ui field" v-if="nodes.length > 0">
|
||||||
|
<combo-box title="节点" name="nodeId" placeholder="节点名称" :v-items="nodes" :v-value="vNodeId"></combo-box>
|
||||||
|
</div>
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<button class="ui button small" type="submit">查找</button>
|
<button class="ui button small" type="submit">搜索日志</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
{$template "/datepicker"}
|
{$template "/datepicker"}
|
||||||
|
|
||||||
<first-menu>
|
<first-menu>
|
||||||
<menu-item :href="path + '?serverId=' + serverId + '&day=' + day + '&keyword=' + keyword + '&ip=' + ip + '&domain=' + domain + '&pageSize=' + pageSize" :active="hasError == 0 && hasWAF == 0">所有日志</menu-item>
|
<menu-item :href="path + '?clusterId=' + clusterId + '&nodeId=' + nodeId + '&serverId=' + serverId + '&day=' + day + '&keyword=' + keyword + '&ip=' + ip + '&domain=' + domain + '&pageSize=' + pageSize" :active="hasError == 0 && hasWAF == 0">所有日志</menu-item>
|
||||||
<menu-item :href="path + '?serverId=' + serverId + '&day=' + day + '&hasError=1' + '&keyword=' + keyword + '&ip=' + ip + '&domain=' + domain + '&pageSize=' + pageSize" :active="hasError > 0">错误日志</menu-item>
|
<menu-item :href="path + '?clusterId=' + clusterId + '&nodeId=' + nodeId + '&serverId=' + serverId + '&day=' + day + '&hasError=1' + '&keyword=' + keyword + '&ip=' + ip + '&domain=' + domain + '&pageSize=' + pageSize" :active="hasError > 0">错误日志</menu-item>
|
||||||
<menu-item :href="path + '?serverId=' + serverId + '&day=' + day + '&hasWAF=1' + '&keyword=' + keyword + '&ip=' + ip + '&domain=' + domain + '&pageSize=' + pageSize" :active="hasWAF > 0">WAF日志</menu-item>
|
<menu-item :href="path + '?clusterId=' + clusterId + '&nodeId=' + nodeId + '&serverId=' + serverId + '&day=' + day + '&hasWAF=1' + '&keyword=' + keyword + '&ip=' + ip + '&domain=' + domain + '&pageSize=' + pageSize" :active="hasWAF > 0">WAF日志</menu-item>
|
||||||
<menu-item :href="'/servers/logs/settings'" code="settings">设置</menu-item>
|
<menu-item :href="'/servers/logs/settings'" code="settings">设置</menu-item>
|
||||||
</first-menu>
|
</first-menu>
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<input type="hidden" name="serverId" :value="serverId"/>
|
<input type="hidden" name="serverId" :value="serverId"/>
|
||||||
<input type="hidden" name="hasError" :value="hasError"/>
|
<input type="hidden" name="hasError" :value="hasError"/>
|
||||||
<input type="hidden" name="hasWAF" :value="hasWAF"/>
|
<input type="hidden" name="hasWAF" :value="hasWAF"/>
|
||||||
<http-access-log-search-box :v-ip="ip" :v-domain="domain" :v-keyword="keyword">
|
<http-access-log-search-box :v-clusters="clusters" :v-cluster-id="clusterId" :v-node-id="nodeId" :v-ip="ip" :v-domain="domain" :v-keyword="keyword">
|
||||||
<div class="ui field">
|
<div class="ui field">
|
||||||
<input type="text" name="day" maxlength="10" placeholder="选择日期" style="width:7.8em" id="day-input" v-model="day"/>
|
<input type="text" name="day" maxlength="10" placeholder="选择日期" style="width:7.8em" id="day-input" v-model="day"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,10 +31,10 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div v-if="accessLogs.length > 0">
|
<div v-if="accessLogs.length > 0">
|
||||||
<a :href="path + '?serverId=' + serverId + '&requestId=' + lastRequestId + '&day=' + day + '&hasError=' + hasError + '&hasWAF=' + hasWAF + '&keyword=' + keyword + '&ip=' + ip + '&domain=' + domain + '&pageSize=' + pageSize" v-if="hasPrev">上一页</a>
|
<a :href="path + '?clusterId=' + clusterId + '&nodeId=' + nodeId + '&serverId=' + serverId + '&requestId=' + lastRequestId + '&day=' + day + '&hasError=' + hasError + '&hasWAF=' + hasWAF + '&keyword=' + keyword + '&ip=' + ip + '&domain=' + domain + '&pageSize=' + pageSize" v-if="hasPrev">上一页</a>
|
||||||
<span v-else class="disabled">上一页</span>
|
<span v-else class="disabled">上一页</span>
|
||||||
<span class="disabled"> | </span>
|
<span class="disabled"> | </span>
|
||||||
<a :href="path + '?serverId=' + serverId + '&requestId=' + nextRequestId + '&day=' + day + '&hasError=' + hasError + '&hasWAF=' + hasWAF + '&keyword=' + keyword + '&ip=' + ip + '&domain=' + domain + '&pageSize=' + pageSize" v-if="hasMore">下一页</a>
|
<a :href="path + '?clusterId=' + clusterId + '&nodeId=' + nodeId + '&serverId=' + serverId + '&requestId=' + nextRequestId + '&day=' + day + '&hasError=' + hasError + '&hasWAF=' + hasWAF + '&keyword=' + keyword + '&ip=' + ip + '&domain=' + domain + '&pageSize=' + pageSize" v-if="hasMore">下一页</a>
|
||||||
<span v-else class="disabled">下一页</span>
|
<span v-else class="disabled">下一页</span>
|
||||||
|
|
||||||
<page-size-selector></page-size-selector>
|
<page-size-selector></page-size-selector>
|
||||||
|
|||||||
Reference in New Issue
Block a user