实现Dashboard

This commit is contained in:
刘祥超
2021-01-21 18:55:53 +08:00
parent beab50de4c
commit bbf7e2898f
11 changed files with 328 additions and 18 deletions

View File

@@ -10,15 +10,16 @@ import (
type AdminModuleCode = string type AdminModuleCode = string
const ( const (
AdminModuleCodeServer AdminModuleCode = "server" // 网站 AdminModuleCodeDashboard AdminModuleCode = "dashboard" // 看板
AdminModuleCodeNode AdminModuleCode = "node" // 节点 AdminModuleCodeServer AdminModuleCode = "server" // 网站
AdminModuleCodeDNS AdminModuleCode = "dns" // DNS AdminModuleCodeNode AdminModuleCode = "node" // 节点
AdminModuleCodeAdmin AdminModuleCode = "admin" // 系统用户 AdminModuleCodeDNS AdminModuleCode = "dns" // DNS
AdminModuleCodeUser AdminModuleCode = "user" // 平台用户 AdminModuleCodeAdmin AdminModuleCode = "admin" // 系统用户
AdminModuleCodeFinance AdminModuleCode = "finance" // 财务 AdminModuleCodeUser AdminModuleCode = "user" // 平台用户
AdminModuleCodeLog AdminModuleCode = "log" // 日志 AdminModuleCodeFinance AdminModuleCode = "finance" // 财务
AdminModuleCodeSetting AdminModuleCode = "setting" // 设置 AdminModuleCodeLog AdminModuleCode = "log" // 日志
AdminModuleCodeCommon AdminModuleCode = "common" // 只要登录就可以访问的模块 AdminModuleCodeSetting AdminModuleCode = "setting" // 设置
AdminModuleCodeCommon AdminModuleCode = "common" // 只要登录就可以访问的模块
) )
var sharedAdminModuleMapping = map[int64]*AdminModuleList{} // adminId => AdminModuleList var sharedAdminModuleMapping = map[int64]*AdminModuleList{} // adminId => AdminModuleList
@@ -109,7 +110,7 @@ func FindFirstAdminModule(adminId int64) (module AdminModuleCode, ok bool) {
list, ok2 := sharedAdminModuleMapping[adminId] list, ok2 := sharedAdminModuleMapping[adminId]
if ok2 { if ok2 {
if list.IsSuper { if list.IsSuper {
return AdminModuleCodeServer, true return AdminModuleCodeDashboard, true
} else if len(list.Modules) > 0 { } else if len(list.Modules) > 0 {
return list.Modules[0].Code, true return list.Modules[0].Code, true
} }
@@ -132,6 +133,11 @@ func FindAdminFullname(adminId int64) string {
// 所有权限列表 // 所有权限列表
func AllModuleMaps() []maps.Map { func AllModuleMaps() []maps.Map {
return []maps.Map{ return []maps.Map{
{
"name": "看板",
"code": AdminModuleCodeDashboard,
"url": "/dashboard",
},
{ {
"name": "网站服务", "name": "网站服务",
"code": AdminModuleCodeServer, "code": AdminModuleCodeServer,

View File

@@ -17,12 +17,26 @@ func FormatBytes(bytes int64) string {
if bytes < 1024 { if bytes < 1024 {
return FormatInt64(bytes) + "B" return FormatInt64(bytes) + "B"
} else if bytes < 1024*1024 { } else if bytes < 1024*1024 {
return fmt.Sprintf("%.2fK", float64(bytes)/1024) return fmt.Sprintf("%.2fKB", float64(bytes)/1024)
} else if bytes < 1024*1024*1024 { } else if bytes < 1024*1024*1024 {
return fmt.Sprintf("%.2fM", float64(bytes)/1024/1024) return fmt.Sprintf("%.2fMB", float64(bytes)/1024/1024)
} else if bytes < 1024*1024*1024*1024 { } else if bytes < 1024*1024*1024*1024 {
return fmt.Sprintf("%.2fG", float64(bytes)/1024/1024/1024) return fmt.Sprintf("%.2fGB", float64(bytes)/1024/1024/1024)
} else { } else {
return fmt.Sprintf("%.2fP", float64(bytes)/1024/1024/1024/1024) return fmt.Sprintf("%.2fPB", float64(bytes)/1024/1024/1024/1024)
}
}
func FormatBits(bits int64) string {
if bits < 1000 {
return FormatInt64(bits) + "B"
} else if bits < 1000*1000 {
return fmt.Sprintf("%.2fKB", float64(bits)/1000)
} else if bits < 1000*1000*1000 {
return fmt.Sprintf("%.2fMB", float64(bits)/1000/1000)
} else if bits < 1000*1000*1000*1000 {
return fmt.Sprintf("%.2fGB", float64(bits)/1000/10001000)
} else {
return fmt.Sprintf("%.2fPB", float64(bits)/1000/1000/1000/1000)
} }
} }

View File

@@ -2,7 +2,12 @@ package dashboard
import ( import (
"github.com/TeaOSLab/EdgeAdmin/internal/configloaders" "github.com/TeaOSLab/EdgeAdmin/internal/configloaders"
"github.com/TeaOSLab/EdgeAdmin/internal/utils/numberutils"
"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/iwind/TeaGo/maps"
"math"
"regexp"
) )
type IndexAction struct { type IndexAction struct {
@@ -17,13 +22,70 @@ func (this *IndexAction) RunGet(params struct{}) {
// 取得用户的权限 // 取得用户的权限
module, ok := configloaders.FindFirstAdminModule(this.AdminId()) module, ok := configloaders.FindFirstAdminModule(this.AdminId())
if ok { if ok {
for _, m := range configloaders.AllModuleMaps() { if module != "dashboard" {
if m.GetString("code") == module { for _, m := range configloaders.AllModuleMaps() {
this.RedirectURL(m.GetString("url")) if m.GetString("code") == module {
return this.RedirectURL(m.GetString("url"))
return
}
} }
} }
} }
// 读取看板数据
resp, err := this.RPC().AdminRPC().ComposeAdminDashboard(this.AdminContext(), &pb.ComposeAdminDashboardRequest{})
if err != nil {
this.ErrorPage(err)
return
}
this.Data["dashboard"] = maps.Map{
"countServers": resp.CountServers,
"countNodeClusters": resp.CountNodeClusters,
"countNodes": resp.CountNodes,
"countUsers": resp.CountUsers,
"countAPINodes": resp.CountAPINodes,
"countDBNodes": resp.CountDBNodes,
"countUserNodes": resp.CountUserNodes,
}
// 今日流量
todayTrafficBytes := int64(0)
if len(resp.DailyTrafficStats) > 0 {
todayTrafficBytes = resp.DailyTrafficStats[len(resp.DailyTrafficStats)-1].Bytes
}
todayTrafficString := numberutils.FormatBits(todayTrafficBytes * 8)
result := regexp.MustCompile(`^(?U)(.+)([a-zA-Z]+)$`).FindStringSubmatch(todayTrafficString)
if len(result) > 2 {
this.Data["todayTraffic"] = result[1]
this.Data["todayTrafficUnit"] = result[2]
} else {
this.Data["todayTraffic"] = todayTrafficString
this.Data["todayTrafficUnit"] = ""
}
// 24小时流量趋势
{
statMaps := []maps.Map{}
for _, stat := range resp.HourlyTrafficStats {
statMaps = append(statMaps, maps.Map{
"count": math.Ceil((float64(stat.Bytes)*8/1000/1000/1000)*1000) / 1000,
"hour": stat.Hour[8:],
})
}
this.Data["hourlyTrafficStats"] = statMaps
}
// 15天流量趋势
{
statMaps := []maps.Map{}
for _, stat := range resp.DailyTrafficStats {
statMaps = append(statMaps, maps.Map{
"count": math.Ceil((float64(stat.Bytes)*8/1000/1000/1000)*1000) / 1000,
"day": stat.Day[4:6] + "月" + stat.Day[6:] + "日",
})
}
this.Data["dailyTrafficStats"] = statMaps
}
this.Show() this.Show()
} }

View File

@@ -9,6 +9,7 @@ import (
func init() { func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) { TeaGo.BeforeStart(func(server *TeaGo.Server) {
server.Prefix("/dashboard"). server.Prefix("/dashboard").
Data("teaMenu", "dashboard").
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeCommon)). Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeCommon)).
GetPost("", new(IndexAction)). GetPost("", new(IndexAction)).
EndAll() EndAll()

View File

@@ -130,6 +130,12 @@ func (this *userMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam
// 菜单配置 // 菜单配置
func (this *userMustAuth) modules(adminId int64) []maps.Map { func (this *userMustAuth) modules(adminId int64) []maps.Map {
allMaps := []maps.Map{ allMaps := []maps.Map{
{
"code": "dashboard",
"module": configloaders.AdminModuleCodeDashboard,
"name": "看板",
"icon": "dashboard",
},
{ {
"code": "servers", "code": "servers",
"module": configloaders.AdminModuleCodeServer, "module": configloaders.AdminModuleCodeServer,

16
web/public/js/echarts/echarts.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
.grid {
margin-top: 2em !important;
margin-left: 2em !important;
}
.grid .column {
margin-bottom: 2em;
border-right: 1px #eee solid;
}
.grid .column div.value {
margin-top: 1.5em;
}
.grid .column div.value span {
font-size: 2em;
margin-right: 0.2em;
}
.grid .column.no-border {
border-right: 0;
}
.chart-box {
height: 20em;
}
/*# sourceMappingURL=index.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["index.less"],"names":[],"mappings":"AAAA;EACC,0BAAA;EACA,2BAAA;;AAFD,KAIC;EACC,kBAAA;EACA,4BAAA;;AANF,KAIC,QAIC,IAAG;EACF,iBAAA;;AATH,KAIC,QAIC,IAAG,MAGF;EACC,cAAA;EACA,mBAAA;;AAbJ,KAkBC,QAAO;EACN,eAAA;;AAKF;EACC,YAAA","file":"index.css"}

View File

@@ -1,2 +1,52 @@
{$layout} {$layout}
{$var "header"}
<!-- echart -->
<script type="text/javascript" src="/js/echarts/echarts.min.js"></script>
{$end}
<div class="ui three columns grid">
<div class="ui column">
<h4>当前集群数</h4>
<div class="value"><span>{{dashboard.countNodeClusters}}</span></div>
</div>
<div class="ui column">
<h4>当前边缘节点数</h4>
<div class="value"><span>{{dashboard.countNodes}}</span></div>
</div>
<div class="ui column no-border">
<h4>当前API节点数</h4>
<div class="value"><span>{{dashboard.countAPINodes}}</span></div>
</div>
<div class="ui column">
<h4>当前用户数</h4>
<div class="value"><span>{{dashboard.countUsers}}</span></div>
</div>
<div class="ui column">
<h4>当前服务数</h4>
<div class="value"><span>{{dashboard.countServers}}</span></div>
</div>
<div class="ui column no-border">
<h4>今日总流量</h4>
<div class="value"><span>{{todayTraffic}}</span>{{todayTrafficUnit}}</div>
</div>
</div>
<div class="ui divider"></div>
<div class="ui menu tabular">
<a href="" class="item" :class="{active: trafficTab == 'hourly'}" @click.prevent="selectTrafficTab('hourly')">24小时流量趋势GB</a>
<a href="" class="item" :class="{active: trafficTab == 'daily'}" @click.prevent="selectTrafficTab('daily')">15天流量趋势GB</a>
</div>
<!-- 按小时统计 -->
<div class="chart-box" id="hourly-traffic-chart-box" v-show="trafficTab == 'hourly'"></div>
<!-- 按日统计 -->
<div class="chart-box" id="daily-traffic-chart-box" v-show="trafficTab == 'daily'"></div>

View File

@@ -0,0 +1,105 @@
Tea.context(function () {
this.trafficTab = "hourly"
this.$delay(function () {
this.reloadHourlyTrafficChart()
})
this.selectTrafficTab = function (tab) {
this.trafficTab = tab
if (tab == "hourly") {
} else if (tab == "daily") {
this.$delay(function () {
this.reloadDailyTrafficChart()
})
}
}
this.reloadHourlyTrafficChart = function () {
let chartBox = document.getElementById("hourly-traffic-chart-box")
let chart = echarts.init(chartBox)
let option = {
xAxis: {
data: this.hourlyTrafficStats.map(function (v) {
return v.hour;
})
},
yAxis: {},
tooltip: {
show: true,
trigger: "item",
formatter: "{c} GB"
},
grid: {
left: 40,
top: 10,
right: 20
},
series: [
{
name: "流量",
type: "line",
data: this.hourlyTrafficStats.map(function (v) {
return v.count;
}),
itemStyle: {
color: "#9DD3E8"
},
lineStyle: {
color: "#9DD3E8"
},
areaStyle: {
color: "#9DD3E8"
}
}
],
animation: false
}
chart.setOption(option)
}
this.reloadDailyTrafficChart = function () {
let chartBox = document.getElementById("daily-traffic-chart-box")
let chart = echarts.init(chartBox)
let option = {
xAxis: {
data: this.dailyTrafficStats.map(function (v) {
return v.day;
})
},
yAxis: {},
tooltip: {
show: true,
trigger: "item",
formatter: "{c} GB"
},
grid: {
left: 40,
top: 10,
right: 20
},
series: [
{
name: "流量",
type: "line",
data: this.dailyTrafficStats.map(function (v) {
return v.count;
}),
itemStyle: {
color: "#9DD3E8"
},
lineStyle: {
color: "#9DD3E8"
},
areaStyle: {
color: "#9DD3E8"
}
}
],
animation: false
}
chart.setOption(option)
}
})

View File

@@ -0,0 +1,27 @@
.grid {
margin-top: 2em !important;
margin-left: 2em !important;
.column {
margin-bottom: 2em;
border-right: 1px #eee solid;
div.value {
margin-top: 1.5em;
span {
font-size: 2em;
margin-right: 0.2em;
}
}
}
.column.no-border {
border-right: 0;
}
}
.chart-box {
height: 20em;
}