Compare commits

...

13 Commits

Author SHA1 Message Date
刘祥超
4ed4f165ac URL跳转可以设置是否保留参数 2022-02-20 09:17:30 +08:00
刘祥超
eb8419fda0 Update components.js 2022-02-17 17:24:23 +08:00
刘祥超
b4a7ee0e89 域名支持--(连续的连字符) 2022-02-17 17:24:14 +08:00
刘祥超
83ad1bc529 优化界面 2022-02-15 14:54:45 +08:00
刘祥超
56948d3035 优化菜单中badge显示 2022-02-14 08:58:31 +08:00
刘祥超
e71e5ad57e 支持默认价格设置 2022-01-23 20:16:02 +08:00
刘祥超
e06d1fa5b0 实现带宽计费套餐 2022-01-23 19:16:22 +08:00
刘祥超
460439f6bd 修复服务访问日志不能使用集群、节点筛选的Bug 2022-01-20 16:28:43 +08:00
刘祥超
4b4072a47e 优化界面 2022-01-20 15:53:46 +08:00
刘祥超
65d19d92e9 增加API方法调用耗时统计 2022-01-19 16:53:38 +08:00
刘祥超
79e52d1c6e 优化demo模式进入命令 2022-01-19 15:58:54 +08:00
刘祥超
59963ee7b9 修复检查更新配置不起作用的Bug 2022-01-18 19:29:42 +08:00
刘祥超
88273b1c9b 修改版本为v0.4.1 2022-01-17 10:53:28 +08:00
33 changed files with 704 additions and 58 deletions

View File

@@ -9,6 +9,7 @@ import (
"github.com/TeaOSLab/EdgeAdmin/internal/nodes"
_ "github.com/TeaOSLab/EdgeAdmin/internal/web"
_ "github.com/iwind/TeaGo/bootstrap"
"github.com/iwind/TeaGo/maps"
"github.com/iwind/gosock/pkg/gosock"
)
@@ -64,12 +65,17 @@ func main() {
fmt.Println("[ERROR]the service not started yet, you should start the service first")
return
}
_, err := sock.Send(&gosock.Command{Code: "demo"})
reply, err := sock.Send(&gosock.Command{Code: "demo"})
if err != nil {
fmt.Println("[ERROR]change demo mode failed: " + err.Error())
return
}
fmt.Println("change demo mode successfully")
var isDemo = maps.NewMap(reply.Params).GetBool("isDemo")
if isDemo {
fmt.Println("change demo mode to: on")
} else {
fmt.Println("change demo mode to: off")
}
})
app.On("generate", func() {
err := gen.Generate()

View File

@@ -1,9 +1,9 @@
package teaconst
const (
Version = "0.4.0"
Version = "0.4.1"
APINodeVersion = "0.4.0"
APINodeVersion = "0.4.1"
ProductName = "Edge Admin"
ProcessName = "edge-admin"

View File

@@ -325,7 +325,9 @@ func (this *AdminNode) listenSock() error {
_ = cmd.ReplyOk()
case "demo":
teaconst.IsDemoMode = !teaconst.IsDemoMode
_ = cmd.ReplyOk()
_ = cmd.Reply(&gosock.Command{
Params: map[string]interface{}{"isDemo": teaconst.IsDemoMode},
})
case "info":
exePath, _ := os.Executable()
_ = cmd.Reply(&gosock.Command{

View File

@@ -161,6 +161,10 @@ func (this *RPCClient) APINodeRPC() pb.APINodeServiceClient {
return pb.NewAPINodeServiceClient(this.pickConn())
}
func (this *RPCClient) APIMethodStatRPC() pb.APIMethodStatServiceClient {
return pb.NewAPIMethodStatServiceClient(this.pickConn())
}
func (this *RPCClient) UserNodeRPC() pb.UserNodeServiceClient {
return pb.NewUserNodeServiceClient(this.pickConn())
}
@@ -382,6 +386,10 @@ func (this *RPCClient) UserBillRPC() pb.UserBillServiceClient {
return pb.NewUserBillServiceClient(this.pickConn())
}
func (this *RPCClient) ServerBillRPC() pb.ServerBillServiceClient {
return pb.NewServerBillServiceClient(this.pickConn())
}
func (this *RPCClient) UserAccountRPC() pb.UserAccountServiceClient {
return pb.NewUserAccountServiceClient(this.pickConn())
}

View File

@@ -8,6 +8,9 @@ import (
teaconst "github.com/TeaOSLab/EdgeAdmin/internal/const"
"github.com/TeaOSLab/EdgeAdmin/internal/events"
"github.com/TeaOSLab/EdgeAdmin/internal/goman"
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
stringutil "github.com/iwind/TeaGo/utils/string"
@@ -46,6 +49,30 @@ func (this *CheckUpdatesTask) Start() {
}
func (this *CheckUpdatesTask) Loop() error {
// 检查是否开启
rpcClient, err := rpc.SharedRPC()
if err != nil {
return err
}
valueResp, err := rpcClient.SysSettingRPC().ReadSysSetting(rpcClient.Context(0), &pb.ReadSysSettingRequest{Code: systemconfigs.SettingCodeCheckUpdates})
if err != nil {
return err
}
var valueJSON = valueResp.ValueJSON
var config = &systemconfigs.CheckUpdatesConfig{AutoCheck: false}
if len(valueJSON) > 0 {
err = json.Unmarshal(valueJSON, config)
if err != nil {
return errors.New("decode config failed: " + err.Error())
}
if !config.AutoCheck {
return nil
}
} else {
return nil
}
// 开始检查
type Response struct {
Code int `json:"code"`
Message string `json:"message"`

View File

@@ -9,6 +9,7 @@ import (
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
"github.com/iwind/TeaGo/logs"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
"time"
)
@@ -26,11 +27,11 @@ func (this *IndexAction) RunGet(params struct{}) {
this.ErrorPage(err)
return
}
count := countResp.Count
page := this.NewPage(count)
var count = countResp.Count
var page = this.NewPage(count)
this.Data["page"] = page.AsHTML()
nodeMaps := []maps.Map{}
var nodeMaps = []maps.Map{}
if count > 0 {
nodesResp, err := this.RPC().APINodeRPC().ListEnabledAPINodes(this.AdminContext(), &pb.ListEnabledAPINodesRequest{
Offset: page.Offset,
@@ -106,5 +107,13 @@ func (this *IndexAction) RunGet(params struct{}) {
}
this.Data["nodes"] = nodeMaps
// 检查是否有调试数据
countMethodStatsResp, err := this.RPC().APIMethodStatRPC().CountAPIMethodStatsWithDay(this.AdminContext(), &pb.CountAPIMethodStatsWithDayRequest{Day: timeutil.Format("Ymd")})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["hasMethodStats"] = countMethodStatsResp.Count > 0
this.Show()
}

View File

@@ -16,6 +16,7 @@ func init() {
Helper(settingutils.NewAdvancedHelper("apiNodes")).
Prefix("/api").
Get("", new(IndexAction)).
Get("/methodStats", new(MethodStatsAction)).
GetPost("/node/createPopup", new(node.CreatePopupAction)).
Post("/delete", new(DeleteAction)).
EndAll()

View File

@@ -0,0 +1,78 @@
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package api
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
timeutil "github.com/iwind/TeaGo/utils/time"
"sort"
"strings"
)
type MethodStatsAction struct {
actionutils.ParentAction
}
func (this *MethodStatsAction) Init() {
this.Nav("", "", "")
}
func (this *MethodStatsAction) RunGet(params struct {
Order string
Method string
Tag string
}) {
this.Data["order"] = params.Order
this.Data["method"] = params.Method
this.Data["tag"] = params.Tag
statsResp, err := this.RPC().APIMethodStatRPC().FindAPIMethodStatsWithDay(this.AdminContext(), &pb.FindAPIMethodStatsWithDayRequest{Day: timeutil.Format("Ymd")})
if err != nil {
this.ErrorPage(err)
return
}
var pbStats = statsResp.ApiMethodStats
switch params.Order {
case "method":
sort.Slice(pbStats, func(i, j int) bool {
return pbStats[i].Method < pbStats[j].Method
})
case "costMs.desc":
sort.Slice(pbStats, func(i, j int) bool {
return pbStats[i].CostMs > pbStats[j].CostMs
})
case "peekMs.desc":
sort.Slice(pbStats, func(i, j int) bool {
return pbStats[i].PeekMs > pbStats[j].PeekMs
})
case "calls.desc":
sort.Slice(pbStats, func(i, j int) bool {
return pbStats[i].CountCalls > pbStats[j].CountCalls
})
}
var statMaps = []maps.Map{}
for _, stat := range pbStats {
if len(params.Method) > 0 && !strings.Contains(strings.ToLower(stat.Method), strings.ToLower(params.Method)) {
continue
}
if len(params.Tag) > 0 && !strings.Contains(strings.ToLower(stat.Tag), strings.ToLower(params.Tag)) {
continue
}
statMaps = append(statMaps, maps.Map{
"id": stat.Id,
"method": stat.Method,
"tag": stat.Tag,
"costMs": stat.CostMs,
"peekMs": stat.PeekMs,
"countCalls": stat.CountCalls,
})
}
this.Data["stats"] = statMaps
this.Show()
}

View File

@@ -17,7 +17,7 @@ func ValidateDomainFormat(domain string) bool {
if piece == "-" ||
strings.HasPrefix(piece, "-") ||
strings.HasSuffix(piece, "-") ||
strings.Contains(piece, "--") ||
//strings.Contains(piece, "--") ||
len(piece) > 63 ||
!regexp.MustCompile(`^[a-z0-9-]+$`).MatchString(piece) {
return false
@@ -75,7 +75,7 @@ func ValidateRecordName(name string) bool {
if piece == "-" ||
strings.HasPrefix(piece, "-") ||
strings.HasSuffix(piece, "-") ||
strings.Contains(piece, "--") ||
//strings.Contains(piece, "--") ||
len(piece) > 63 ||
!regexp.MustCompile(`^[_a-z0-9-]+$`).MatchString(piece) {
return false

View File

@@ -29,6 +29,9 @@ func (this *HistoryAction) RunGet(params struct {
RequestId string
HasError int
ClusterId int64
NodeId int64
PageSize int
}) {
if len(params.Day) == 0 {
@@ -44,6 +47,8 @@ func (this *HistoryAction) RunGet(params struct {
this.Data["hasError"] = params.HasError
this.Data["hasWAF"] = params.HasWAF
this.Data["pageSize"] = params.PageSize
this.Data["clusterId"] = params.ClusterId
this.Data["nodeId"] = params.NodeId
day := params.Day
ipList := []string{}
@@ -66,6 +71,8 @@ func (this *HistoryAction) RunGet(params struct {
Keyword: params.Keyword,
Ip: params.Ip,
Domain: params.Domain,
NodeId: params.NodeId,
NodeClusterId: params.ClusterId,
Size: size,
})
if err != nil {
@@ -102,6 +109,8 @@ func (this *HistoryAction) RunGet(params struct {
Keyword: params.Keyword,
Ip: params.Ip,
Domain: params.Domain,
NodeId: params.NodeId,
NodeClusterId: params.ClusterId,
Size: size,
Reverse: true,
})

View File

@@ -22,6 +22,8 @@ func (this *IndexAction) RunGet(params struct {
RequestId string
Ip string
Domain string
ClusterId int64
NodeId int64
Keyword string
}) {
this.Data["serverId"] = params.ServerId
@@ -30,6 +32,8 @@ func (this *IndexAction) RunGet(params struct {
this.Data["domain"] = params.Domain
this.Data["keyword"] = params.Keyword
this.Data["path"] = this.Request.URL.Path
this.Data["clusterId"] = params.ClusterId
this.Data["nodeId"] = params.NodeId
// 记录最近使用
_, err := this.RPC().LatestItemRPC().IncreaseLatestItem(this.AdminContext(), &pb.IncreaseLatestItemRequest{
@@ -50,19 +54,23 @@ func (this *IndexAction) RunPost(params struct {
Keyword string
Ip string
Domain string
ClusterId int64
NodeId int64
Must *actions.Must
}) {
isReverse := len(params.RequestId) > 0
accessLogsResp, err := this.RPC().HTTPAccessLogRPC().ListHTTPAccessLogs(this.AdminContext(), &pb.ListHTTPAccessLogsRequest{
ServerId: params.ServerId,
RequestId: params.RequestId,
Size: 20,
Day: timeutil.Format("Ymd"),
Keyword: params.Keyword,
Ip: params.Ip,
Domain: params.Domain,
Reverse: isReverse,
ServerId: params.ServerId,
RequestId: params.RequestId,
Size: 20,
Day: timeutil.Format("Ymd"),
Keyword: params.Keyword,
Ip: params.Ip,
Domain: params.Domain,
NodeId: params.NodeId,
NodeClusterId: params.ClusterId,
Reverse: isReverse,
})
if err != nil {
this.ErrorPage(err)

View File

@@ -24,6 +24,8 @@ func (this *TodayAction) RunGet(params struct {
Keyword string
Ip string
Domain string
ClusterId int64
NodeId int64
PageSize int
}) {
@@ -40,6 +42,8 @@ func (this *TodayAction) RunGet(params struct {
this.Data["ip"] = params.Ip
this.Data["domain"] = params.Domain
this.Data["hasWAF"] = params.HasWAF
this.Data["clusterId"] = params.ClusterId
this.Data["nodeId"] = params.NodeId
resp, err := this.RPC().HTTPAccessLogRPC().ListHTTPAccessLogs(this.AdminContext(), &pb.ListHTTPAccessLogsRequest{
RequestId: params.RequestId,
@@ -50,6 +54,8 @@ func (this *TodayAction) RunGet(params struct {
Keyword: params.Keyword,
Ip: params.Ip,
Domain: params.Domain,
NodeId: params.NodeId,
NodeClusterId: params.ClusterId,
Size: size,
})
if err != nil {
@@ -87,6 +93,8 @@ func (this *TodayAction) RunGet(params struct {
Keyword: params.Keyword,
Ip: params.Ip,
Domain: params.Domain,
NodeId: params.NodeId,
NodeClusterId: params.ClusterId,
Size: size,
Reverse: true,
})

View File

@@ -27,11 +27,13 @@ func (this *CreatePopupAction) RunGet(params struct {
}
func (this *CreatePopupAction) RunPost(params struct {
Mode string
BeforeURL string
AfterURL string
MatchPrefix bool
MatchRegexp bool
KeepRequestURI bool
KeepArgs bool
Status int
CondsJSON []byte
IsOn bool
@@ -99,12 +101,14 @@ func (this *CreatePopupAction) RunPost(params struct {
}
this.Data["redirect"] = maps.Map{
"mode": params.Mode,
"status": params.Status,
"beforeURL": params.BeforeURL,
"afterURL": params.AfterURL,
"matchPrefix": params.MatchPrefix,
"matchRegexp": params.MatchRegexp,
"keepRequestURI": params.KeepRequestURI,
"keepArgs": params.KeepArgs,
"conds": conds,
"isOn": params.IsOn,
}

View File

@@ -1370,25 +1370,182 @@ Vue.component("plan-price-view", {
},
template: `<div>
<span v-if="plan.priceType == 'period'">
<span v-if="plan.monthlyPrice > 0">月度:¥{{plan.monthlyPrice}}元<br/></span>
<span v-if="plan.seasonallyPrice > 0">季度:¥{{plan.seasonallyPrice}}元<br/></span>
<span v-if="plan.yearlyPrice > 0">年度:¥{{plan.yearlyPrice}}元</span>
按时间周期计费
<div>
<span class="grey small">
<span v-if="plan.monthlyPrice > 0">月度:¥{{plan.monthlyPrice}}元<br/></span>
<span v-if="plan.seasonallyPrice > 0">季度:¥{{plan.seasonallyPrice}}元<br/></span>
<span v-if="plan.yearlyPrice > 0">年度:¥{{plan.yearlyPrice}}元</span>
</span>
</div>
</span>
<span v-if="plan.priceType == 'traffic'">
基础价格:¥{{plan.trafficPrice.base}}元/GB
按流量计费
<div>
<span class="grey small">基础价格:¥{{plan.trafficPrice.base}}元/GB</span>
</div>
</span>
<div v-if="plan.priceType == 'bandwidth' && plan.bandwidthPrice != null && plan.bandwidthPrice.percentile > 0">
按{{plan.bandwidthPrice.percentile}}th带宽计费
<div>
<div v-for="range in plan.bandwidthPrice.ranges">
<span class="small grey">{{range.minMB}} - <span v-if="range.maxMB > 0">{{range.maxMB}}MB</span><span v-else>&infin;</span> {{range.pricePerMB}}元/MB</span>
</div>
</div>
</div>
</div>`
})
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: "",
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.ranges.push(this.addingRange)
this.ranges.$sort(function (v1, v2) {
if (v1.minMB < v2.minMB) {
return -1
}
if (v1.minMB == v2.minMB) {
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
}
},
template: `<div>
<!-- 已有价格 -->
<div v-if="ranges.length > 0">
<div class="ui label basic small" v-for="(range, index) in ranges" style="margin-bottom: 0.5em">
{{range.minMB}}MB - <span v-if="range.maxMB > 0">{{range.maxMB}}MB</span><span v-else>&infin;</span> &nbsp; 价格:{{range.pricePerMB}}元/MB
&nbsp; <a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
</div>
<div class="ui divider"></div>
</div>
<!-- 添加 -->
<div v-if="isAdding">
<table class="ui table">
<tr>
<td class="title">带宽下限</td>
<td>
<div class="ui input right labeled">
<input type="text" placeholder="最小带宽" style="width: 7em" maxlength="10" ref="minMB" @keyup.enter="confirm()" @keypress.enter.prevent="1" v-model="minMB"/>
<span class="ui label">MB</span>
</div>
</td>
</tr>
<tr>
<td class="title">带宽上限</td>
<td>
<div class="ui input right labeled">
<input type="text" placeholder="最大带宽" style="width: 7em" maxlength="10" @keyup.enter="confirm()" @keypress.enter.prevent="1" v-model="maxMB"/>
<span class="ui label">MB</span>
</div>
<p class="comment">如果填0表示上不封顶。</p>
</td>
</tr>
<tr>
<td class="title">单位价格</td>
<td>
<div class="ui input right labeled">
<input type="text" placeholder="单位价格" style="width: 7em" maxlength="10" @keyup.enter="confirm()" @keypress.enter.prevent="1" v-model="pricePerMB"/>
<span class="ui label">元/MB</span>
</div>
</td>
</tr>
</table>
<button class="ui button small" type="button" @click.prevent="confirm">确定</button> &nbsp;
<a href="" title="取消" @click.prevent="cancelAdding"><i class="icon remove small"></i></a>
</div>
<!-- 按钮 -->
<div v-if="!isAdding">
<button class="ui button small" type="button" @click.prevent="add">+</button>
</div>
</div>`
})
// 套餐价格配置
Vue.component("plan-price-config-box", {
props: ["v-price-type", "v-monthly-price", "v-seasonally-price", "v-yearly-price", "v-traffic-price"],
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 = "period"
priceType = "bandwidth"
}
// 按时间周期计费
let monthlyPriceNumber = 0
let monthlyPrice = this.vMonthlyPrice
if (monthlyPrice == null || monthlyPrice <= 0) {
@@ -1425,6 +1582,7 @@ Vue.component("plan-price-config-box", {
}
}
// 按流量计费
let trafficPrice = this.vTrafficPrice
let trafficPriceBaseNumber = 0
if (trafficPrice != null) {
@@ -1439,6 +1597,17 @@ Vue.component("plan-price-config-box", {
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,
@@ -1450,7 +1619,15 @@ Vue.component("plan-price-config-box", {
yearlyPriceNumber: yearlyPriceNumber,
trafficPriceBase: trafficPriceBase,
trafficPrice: trafficPrice
trafficPrice: trafficPrice,
bandwidthPrice: bandwidthPrice,
bandwidthPercentile: bandwidthPrice.percentile
}
},
methods: {
changeBandwidthPriceRanges: function (ranges) {
this.bandwidthPrice.ranges = ranges
}
},
watch: {
@@ -1481,6 +1658,15 @@ Vue.component("plan-price-config-box", {
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: `<div>
@@ -1489,10 +1675,12 @@ Vue.component("plan-price-config-box", {
<input type="hidden" name="seasonallyPrice" :value="seasonallyPriceNumber"/>
<input type="hidden" name="yearlyPrice" :value="yearlyPriceNumber"/>
<input type="hidden" name="trafficPriceJSON" :value="JSON.stringify(trafficPrice)"/>
<input type="hidden" name="bandwidthPriceJSON" :value="JSON.stringify(bandwidthPrice)"/>
<div>
<radio :v-value="'period'" :value="priceType" v-model="priceType">&nbsp;按时间周期</radio> &nbsp; &nbsp;
<radio :v-value="'traffic'" :value="priceType" v-model="priceType">&nbsp;按流量</radio>
<radio :v-value="'bandwidth'" :value="priceType" v-model="priceType">&nbsp;按带宽</radio> &nbsp;
<radio :v-value="'traffic'" :value="priceType" v-model="priceType">&nbsp;按流量</radio> &nbsp;
<radio :v-value="'period'" :value="priceType" v-model="priceType" v-show="typeof(vDisablePeriod) != 'boolean' || !vDisablePeriod">&nbsp;按时间周期</radio>
</div>
<!-- 按时间周期 -->
@@ -1534,7 +1722,7 @@ Vue.component("plan-price-config-box", {
<div class="ui divider"></div>
<table class="ui table">
<tr>
<td class="title">基础流量费用</td>
<td class="title">基础流量费用 *</td>
<td>
<div class="ui input right labeled">
<input type="text" v-model="trafficPriceBase" maxlength="10" style="width: 7em"/>
@@ -1544,6 +1732,28 @@ Vue.component("plan-price-config-box", {
</tr>
</table>
</div>
<!-- 按带宽 -->
<div v-show="priceType == 'bandwidth'">
<div class="ui divider"></div>
<table class="ui table">
<tr>
<td class="title">带宽百分位 *</td>
<td>
<div class="ui input right labeled">
<input type="text" style="width: 4em" maxlength="3" v-model="bandwidthPercentile"/>
<span class="ui label">th</span>
</div>
</td>
</tr>
<tr>
<td>带宽价格</td>
<td>
<plan-bandwidth-ranges :v-ranges="bandwidthPrice.ranges" @change="changeBandwidthPriceRanges"></plan-bandwidth-ranges>
</td>
</tr>
</table>
</div>
</div>`
})
@@ -1647,7 +1857,7 @@ Vue.component("http-request-conds-box", {
<table class="ui table">
<tr v-for="(group, groupIndex) in conds.groups">
<td class="title" :class="{'color-border':conds.connector == 'and'}" :style="{'border-bottom':(groupIndex < conds.groups.length-1) ? '1px solid rgba(34,36,38,.15)':''}">分组{{groupIndex+1}}</td>
<td style="background: white;" :style="{'border-bottom':(groupIndex < conds.groups.length-1) ? '1px solid rgba(34,36,38,.15)':''}">
<td style="background: white; word-break: break-all" :style="{'border-bottom':(groupIndex < conds.groups.length-1) ? '1px solid rgba(34,36,38,.15)':''}">
<var v-for="(cond, index) in group.conds" style="font-style: normal;display: inline-block; margin-bottom:0.5em">
<span class="ui label tiny">
<var v-if="cond.type.length == 0 || cond.type == 'params'" style="font-style: normal">{{cond.param}} <var>{{cond.operator}}</var></var>

View File

@@ -0,0 +1,139 @@
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: "",
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.ranges.push(this.addingRange)
this.ranges.$sort(function (v1, v2) {
if (v1.minMB < v2.minMB) {
return -1
}
if (v1.minMB == v2.minMB) {
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
}
},
template: `<div>
<!-- 已有价格 -->
<div v-if="ranges.length > 0">
<div class="ui label basic small" v-for="(range, index) in ranges" style="margin-bottom: 0.5em">
{{range.minMB}}MB - <span v-if="range.maxMB > 0">{{range.maxMB}}MB</span><span v-else>&infin;</span> &nbsp; 价格:{{range.pricePerMB}}元/MB
&nbsp; <a href="" title="删除" @click.prevent="remove(index)"><i class="icon remove small"></i></a>
</div>
<div class="ui divider"></div>
</div>
<!-- 添加 -->
<div v-if="isAdding">
<table class="ui table">
<tr>
<td class="title">带宽下限</td>
<td>
<div class="ui input right labeled">
<input type="text" placeholder="最小带宽" style="width: 7em" maxlength="10" ref="minMB" @keyup.enter="confirm()" @keypress.enter.prevent="1" v-model="minMB"/>
<span class="ui label">MB</span>
</div>
</td>
</tr>
<tr>
<td class="title">带宽上限</td>
<td>
<div class="ui input right labeled">
<input type="text" placeholder="最大带宽" style="width: 7em" maxlength="10" @keyup.enter="confirm()" @keypress.enter.prevent="1" v-model="maxMB"/>
<span class="ui label">MB</span>
</div>
<p class="comment">如果填0表示上不封顶。</p>
</td>
</tr>
<tr>
<td class="title">单位价格</td>
<td>
<div class="ui input right labeled">
<input type="text" placeholder="单位价格" style="width: 7em" maxlength="10" @keyup.enter="confirm()" @keypress.enter.prevent="1" v-model="pricePerMB"/>
<span class="ui label">元/MB</span>
</div>
</td>
</tr>
</table>
<button class="ui button small" type="button" @click.prevent="confirm">确定</button> &nbsp;
<a href="" title="取消" @click.prevent="cancelAdding"><i class="icon remove small"></i></a>
</div>
<!-- 按钮 -->
<div v-if="!isAdding">
<button class="ui button small" type="button" @click.prevent="add">+</button>
</div>
</div>`
})

View File

@@ -1,12 +1,13 @@
// 套餐价格配置
Vue.component("plan-price-config-box", {
props: ["v-price-type", "v-monthly-price", "v-seasonally-price", "v-yearly-price", "v-traffic-price"],
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 = "period"
priceType = "bandwidth"
}
// 按时间周期计费
let monthlyPriceNumber = 0
let monthlyPrice = this.vMonthlyPrice
if (monthlyPrice == null || monthlyPrice <= 0) {
@@ -43,6 +44,7 @@ Vue.component("plan-price-config-box", {
}
}
// 按流量计费
let trafficPrice = this.vTrafficPrice
let trafficPriceBaseNumber = 0
if (trafficPrice != null) {
@@ -57,6 +59,17 @@ Vue.component("plan-price-config-box", {
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,
@@ -68,7 +81,15 @@ Vue.component("plan-price-config-box", {
yearlyPriceNumber: yearlyPriceNumber,
trafficPriceBase: trafficPriceBase,
trafficPrice: trafficPrice
trafficPrice: trafficPrice,
bandwidthPrice: bandwidthPrice,
bandwidthPercentile: bandwidthPrice.percentile
}
},
methods: {
changeBandwidthPriceRanges: function (ranges) {
this.bandwidthPrice.ranges = ranges
}
},
watch: {
@@ -99,6 +120,15 @@ Vue.component("plan-price-config-box", {
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: `<div>
@@ -107,10 +137,12 @@ Vue.component("plan-price-config-box", {
<input type="hidden" name="seasonallyPrice" :value="seasonallyPriceNumber"/>
<input type="hidden" name="yearlyPrice" :value="yearlyPriceNumber"/>
<input type="hidden" name="trafficPriceJSON" :value="JSON.stringify(trafficPrice)"/>
<input type="hidden" name="bandwidthPriceJSON" :value="JSON.stringify(bandwidthPrice)"/>
<div>
<radio :v-value="'period'" :value="priceType" v-model="priceType">&nbsp;按时间周期</radio> &nbsp; &nbsp;
<radio :v-value="'traffic'" :value="priceType" v-model="priceType">&nbsp;按流量</radio>
<radio :v-value="'bandwidth'" :value="priceType" v-model="priceType">&nbsp;按带宽</radio> &nbsp;
<radio :v-value="'traffic'" :value="priceType" v-model="priceType">&nbsp;按流量</radio> &nbsp;
<radio :v-value="'period'" :value="priceType" v-model="priceType" v-show="typeof(vDisablePeriod) != 'boolean' || !vDisablePeriod">&nbsp;按时间周期</radio>
</div>
<!-- 按时间周期 -->
@@ -152,7 +184,7 @@ Vue.component("plan-price-config-box", {
<div class="ui divider"></div>
<table class="ui table">
<tr>
<td class="title">基础流量费用</td>
<td class="title">基础流量费用 *</td>
<td>
<div class="ui input right labeled">
<input type="text" v-model="trafficPriceBase" maxlength="10" style="width: 7em"/>
@@ -162,5 +194,27 @@ Vue.component("plan-price-config-box", {
</tr>
</table>
</div>
<!-- 按带宽 -->
<div v-show="priceType == 'bandwidth'">
<div class="ui divider"></div>
<table class="ui table">
<tr>
<td class="title">带宽百分位 *</td>
<td>
<div class="ui input right labeled">
<input type="text" style="width: 4em" maxlength="3" v-model="bandwidthPercentile"/>
<span class="ui label">th</span>
</div>
</td>
</tr>
<tr>
<td>带宽价格</td>
<td>
<plan-bandwidth-ranges :v-ranges="bandwidthPrice.ranges" @change="changeBandwidthPriceRanges"></plan-bandwidth-ranges>
</td>
</tr>
</table>
</div>
</div>`
})

View File

@@ -7,12 +7,28 @@ Vue.component("plan-price-view", {
},
template: `<div>
<span v-if="plan.priceType == 'period'">
<span v-if="plan.monthlyPrice > 0">月度:¥{{plan.monthlyPrice}}元<br/></span>
<span v-if="plan.seasonallyPrice > 0">季度:¥{{plan.seasonallyPrice}}元<br/></span>
<span v-if="plan.yearlyPrice > 0">年度:¥{{plan.yearlyPrice}}元</span>
按时间周期计费
<div>
<span class="grey small">
<span v-if="plan.monthlyPrice > 0">月度:¥{{plan.monthlyPrice}}元<br/></span>
<span v-if="plan.seasonallyPrice > 0">季度:¥{{plan.seasonallyPrice}}元<br/></span>
<span v-if="plan.yearlyPrice > 0">年度:¥{{plan.yearlyPrice}}元</span>
</span>
</div>
</span>
<span v-if="plan.priceType == 'traffic'">
基础价格:¥{{plan.trafficPrice.base}}元/GB
按流量计费
<div>
<span class="grey small">基础价格:¥{{plan.trafficPrice.base}}元/GB</span>
</div>
</span>
<div v-if="plan.priceType == 'bandwidth' && plan.bandwidthPrice != null && plan.bandwidthPrice.percentile > 0">
按{{plan.bandwidthPrice.percentile}}th带宽计费
<div>
<div v-for="range in plan.bandwidthPrice.ranges">
<span class="small grey">{{range.minMB}} - <span v-if="range.maxMB > 0">{{range.maxMB}}MB</span><span v-else>&infin;</span> {{range.pricePerMB}}元/MB</span>
</div>
</div>
</div>
</div>`
})

View File

@@ -64,7 +64,7 @@ Vue.component("http-request-conds-box", {
<table class="ui table">
<tr v-for="(group, groupIndex) in conds.groups">
<td class="title" :class="{'color-border':conds.connector == 'and'}" :style="{'border-bottom':(groupIndex < conds.groups.length-1) ? '1px solid rgba(34,36,38,.15)':''}">分组{{groupIndex+1}}</td>
<td style="background: white;" :style="{'border-bottom':(groupIndex < conds.groups.length-1) ? '1px solid rgba(34,36,38,.15)':''}">
<td style="background: white; word-break: break-all" :style="{'border-bottom':(groupIndex < conds.groups.length-1) ? '1px solid rgba(34,36,38,.15)':''}">
<var v-for="(cond, index) in group.conds" style="font-style: normal;display: inline-block; margin-bottom:0.5em">
<span class="ui label tiny">
<var v-if="cond.type.length == 0 || cond.type == 'params'" style="font-style: normal">{{cond.param}} <var>{{cond.operator}}</var></var>

View File

@@ -274,6 +274,12 @@ p.margin {
.field.text {
padding: .5em;
}
.form .fields:not(.inline) .field {
margin-bottom: 0.5em !important;
}
.form .fields:not(.inline) .field .button {
min-width: 5em;
}
/** body **/
@keyframes blink {
from {

File diff suppressed because one or more lines are too long

View File

@@ -93,7 +93,10 @@
</a>
<div v-if="teaMenu == module.code" class="sub-items">
<a class="item" v-for="subItem in module.subItems" v-if="subItem.isOn !== false" :href="subItem.url"><i class="icon angle right" v-if="subItem.code == teaSubMenu"></i> {{subItem.name}}
<span class="ui label tiny red" v-if="subItem.badge != null && subItem.badge > 0">{{subItem.badge}}</span>
<span class="ui label tiny red" v-if="subItem.badge != null && subItem.badge > 0">
<span v-if="subItem.badge < 100">{{subItem.badge}}</span>
<span v-else>99+</span>
</span>
</a>
</div>
</div>

View File

@@ -214,6 +214,16 @@ div.margin, p.margin {
padding: .5em;
}
.form .fields:not(.inline) {
.field {
margin-bottom: 0.5em !important;
.button {
min-width: 5em;
}
}
}
/** body **/
@keyframes blink {
from {

View File

@@ -2,6 +2,7 @@
<first-menu>
<a href="" class="item" @click.prevent="createNode()">[添加节点]</a>
<a href="/api/methodStats" class="item" v-if="hasMethodStats">用时统计</a>
</first-menu>
<p class="comment" v-if="nodes.length == 0">暂时还没有节点。</p>

View File

@@ -0,0 +1,46 @@
{$layout}
<div class="margin"></div>
<form method="get" action="/api/methodStats" class="ui form">
<div class="ui fields inline">
<div class="ui field">
<input type="text" name="method" v-model="method" placeholder="方法"/>
</div>
<div class="ui field">
<input type="text" name="tag" v-model="tag" placeholder="标签"/>
</div>
<div class="ui field">
<select class="ui dropdown auto-width" name="order" v-model="order">
<option value="">[排序]</option>
<option value="method">方法</option>
<option value="costMs.desc">平均耗时</option>
<option value="peekMs.desc">峰值耗时</option>
<option value="calls.desc">调用次数</option>
</select>
</div>
<div class="ui field">
<button class="ui button" type="submit">搜索</button>
&nbsp;
<a href="/api/methodStats" v-if="method.length > 0 || tag.length > 0 || order.length > 0">清除条件</a>
</div>
</div>
</form>
<table class="ui table selectable celled">
<thead>
<tr>
<th>方法</th>
<th>标签</th>
<th>平均耗时</th>
<th>峰值耗时</th>
<td>调用次数</td>
</tr>
</thead>
<tr v-for="stat in stats">
<td>{{stat.method}}</td>
<td>{{stat.tag}}</td>
<td>{{stat.costMs}}ms</td>
<td>{{stat.peekMs}}ms</td>
<td>{{stat.countCalls}}次</td>
</tr>
</table>

View File

@@ -6,7 +6,7 @@
<form method="get" action="/clusters/logs" class="ui form" autocomplete="off">
<input type="hidden" name="type" :value="type"/>
<div class="ui fields inline">
<div class="ui fields">
<div class="ui field">
<input type="text" name="dayFrom" placeholder="开始日期" v-model="dayFrom" value="" style="width:8em" id="day-from-picker"/>
</div>
@@ -31,8 +31,6 @@
<div class="ui field">
<input type="text" name="keyword" style="width:10em" v-model="keyword" placeholder="关键词"/>
</div>
</div>
<div class="ui fields inline" style="margin-top: 0.5em">
<div class="ui field">
<node-cluster-combo-box :v-cluster-id="clusterId" @change="changeCluster"></node-cluster-combo-box>
</div>

View File

@@ -1,6 +0,0 @@
<first-menu>
<menu-item href="." code="daily">按天统计</menu-item>
<menu-item href=".monthly" code="monthly">按月统计</menu-item>
</first-menu>
<div class="margin"></div>

View File

@@ -14,7 +14,7 @@
<input type="hidden" name="serverId" :value="serverId"/>
<input type="hidden" name="hasError" :value="hasError"/>
<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-ip="ip" :v-domain="domain" :v-keyword="keyword" :v-cluster-id="clusterId" :v-node-id="nodeId">
<div class="ui field">
<input type="text" name="day" maxlength="10" placeholder="选择日期" style="width:7.8em" id="day-input" v-model="day"/>
</div>

View File

@@ -4,7 +4,7 @@
<div class="right-box">
<form method="get" class="ui form small" :action="path" autocomplete="off">
<input type="hidden" name="serverId" :value="serverId"/>
<http-access-log-search-box :v-ip="ip" :v-domain="domain" :v-keyword="keyword"></http-access-log-search-box>
<http-access-log-search-box :v-ip="ip" :v-domain="domain" :v-keyword="keyword" :v-cluster-id="clusterId" :v-node-id="nodeId"></http-access-log-search-box>
</form>
<p class="comment" v-if="isLoaded && accessLogs.length == 0">今天暂时还没有访问日志。</p>

View File

@@ -14,7 +14,9 @@ Tea.context(function () {
requestId: this.requestId,
keyword: this.keyword,
ip: this.ip,
domain: this.domain
domain: this.domain,
clusterId: this.clusterId,
nodeId: this.nodeId
})
.success(function (resp) {
this.accessLogs = resp.data.accessLogs.concat(this.accessLogs)

View File

@@ -12,7 +12,7 @@
<input type="hidden" name="serverId" :value="serverId"/>
<input type="hidden" name="hasError" :value="hasError"/>
<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>
<http-access-log-search-box :v-ip="ip" :v-domain="domain" :v-keyword="keyword" :v-cluster-id="clusterId" :v-node-id="nodeId"></http-access-log-search-box>
</form>
<p class="comment" v-if="accessLogs.length == 0">今天暂时还没有访问日志。</p>

View File

@@ -7,7 +7,7 @@
<table class="ui table definition selectable">
<tr>
<td class="title">子条件列表</td>
<td>
<td style="word-break: break-all">
<div v-if="group.conds.length > 0">
<var v-for="(cond, index) in group.conds" style="font-style: normal;display: inline-block; margin-bottom:0.5em">
<span class="ui label small">

View File

@@ -43,6 +43,13 @@
<p class="comment">选中后则跳转之后保留跳转之前的URL路径和参数。</p>
</td>
</tr>
<tr v-if="mode == 'equal' || mode == 'matchRegexp'">
<td>是否保留请求参数</td>
<td>
<checkbox name="keepArgs" value="1" v-model="redirect.keepArgs"></checkbox>
<p class="comment">选中后则跳转之后保留跳转之前的URL上的参数即问号之后的部分</p>
</td>
</tr>
<tr>
<td>跳转状态码</td>
<td>

View File

@@ -11,6 +11,7 @@ Tea.context(function () {
matchPrefix: false,
matchRegexp: false,
keepRequestURI: false,
keepArgs: true,
conds: null,
isOn: true
}
@@ -22,8 +23,7 @@ Tea.context(function () {
} else if (this.redirect.matchRegexp) {
this.mode = "matchRegexp"
} else {
this.mode = "matchPrefix"
this.redirect.matchPrefix = true
this.mode = "equal"
}
this.$delay(function () {