experimental route perf pane
add 1.14 to travis test list more qutils tests add panel_menu_stats_routes_perf phrase
This commit is contained in:
parent
671134b1f3
commit
ca8411a519
|
@ -4,6 +4,7 @@ env:
|
||||||
language: go
|
language: go
|
||||||
go:
|
go:
|
||||||
- "1.13"
|
- "1.13"
|
||||||
|
- "1.14"
|
||||||
- master
|
- master
|
||||||
before_install:
|
before_install:
|
||||||
- cd $HOME
|
- cd $HOME
|
||||||
|
|
|
@ -472,6 +472,19 @@ type PanelAnalyticsRoutesPage struct {
|
||||||
TimeRange string
|
TimeRange string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PanelAnalyticsRoutesPerfItem struct {
|
||||||
|
Route string
|
||||||
|
Count int
|
||||||
|
Unit string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PanelAnalyticsRoutesPerfPage struct {
|
||||||
|
*BasePanelPage
|
||||||
|
ItemList []PanelAnalyticsRoutesPerfItem
|
||||||
|
Graph PanelTimeGraph
|
||||||
|
TimeRange string
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Rename the fields as this structure is being used in a generic way now
|
// TODO: Rename the fields as this structure is being used in a generic way now
|
||||||
type PanelAnalyticsAgentsItem struct {
|
type PanelAnalyticsAgentsItem struct {
|
||||||
Agent string
|
Agent string
|
||||||
|
|
|
@ -98,9 +98,9 @@ func cascadeForumPerms(fp *ForumPerms, u *User) {
|
||||||
|
|
||||||
// Even if they have the right permissions, the control panel is only open to supermods+. There are many areas without subpermissions which assume that the current user is a supermod+ and admins are extremely unlikely to give these permissions to someone who isn't at-least a supermod to begin with
|
// Even if they have the right permissions, the control panel is only open to supermods+. There are many areas without subpermissions which assume that the current user is a supermod+ and admins are extremely unlikely to give these permissions to someone who isn't at-least a supermod to begin with
|
||||||
// TODO: Do a panel specific theme?
|
// TODO: Do a panel specific theme?
|
||||||
func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header *Header, stats PanelStats, rerr RouteError) {
|
func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (h *Header, stats PanelStats, rerr RouteError) {
|
||||||
theme := GetThemeByReq(r)
|
theme := GetThemeByReq(r)
|
||||||
header = &Header{
|
h = &Header{
|
||||||
Site: Site,
|
Site: Site,
|
||||||
Settings: SettingBox.Load().(SettingMap),
|
Settings: SettingBox.Load().(SettingMap),
|
||||||
Themes: Themes,
|
Themes: Themes,
|
||||||
|
@ -110,15 +110,16 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
|
||||||
Zone: "panel",
|
Zone: "panel",
|
||||||
Writer: w,
|
Writer: w,
|
||||||
IsoCode: phrases.GetLangPack().IsoCode,
|
IsoCode: phrases.GetLangPack().IsoCode,
|
||||||
|
StartedAt: time.Now(),
|
||||||
}
|
}
|
||||||
// TODO: We should probably initialise header.ExtData
|
// TODO: We should probably initialise header.ExtData
|
||||||
// ? - Should we only show this in debug mode? It might be useful for detecting issues in production, if we show it there as-well
|
// ? - Should we only show this in debug mode? It might be useful for detecting issues in production, if we show it there as-well
|
||||||
//if user.IsAdmin {
|
//if user.IsAdmin {
|
||||||
header.StartedAt = time.Now()
|
//h.StartedAt = time.Now()
|
||||||
//}
|
//}
|
||||||
|
|
||||||
header.AddSheet(theme.Name + "/main.css")
|
h.AddSheet(theme.Name + "/main.css")
|
||||||
header.AddSheet(theme.Name + "/panel.css")
|
h.AddSheet(theme.Name + "/panel.css")
|
||||||
if len(theme.Resources) > 0 {
|
if len(theme.Resources) > 0 {
|
||||||
rlist := theme.Resources
|
rlist := theme.Resources
|
||||||
for _, res := range rlist {
|
for _, res := range rlist {
|
||||||
|
@ -126,12 +127,12 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
|
||||||
extarr := strings.Split(res.Name, ".")
|
extarr := strings.Split(res.Name, ".")
|
||||||
ext := extarr[len(extarr)-1]
|
ext := extarr[len(extarr)-1]
|
||||||
if ext == "css" {
|
if ext == "css" {
|
||||||
header.AddSheet(res.Name)
|
h.AddSheet(res.Name)
|
||||||
} else if ext == "js" {
|
} else if ext == "js" {
|
||||||
if res.Async {
|
if res.Async {
|
||||||
header.AddScriptAsync(res.Name)
|
h.AddScriptAsync(res.Name)
|
||||||
} else {
|
} else {
|
||||||
header.AddScript(res.Name)
|
h.AddScript(res.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,7 +147,7 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
|
||||||
stats.Groups = Groups.Count()
|
stats.Groups = Groups.Count()
|
||||||
stats.Forums = Forums.Count()
|
stats.Forums = Forums.Count()
|
||||||
stats.Pages = Pages.Count()
|
stats.Pages = Pages.Count()
|
||||||
stats.Settings = len(header.Settings)
|
stats.Settings = len(h.Settings)
|
||||||
stats.WordFilters = WordFilters.EstCount()
|
stats.WordFilters = WordFilters.EstCount()
|
||||||
stats.Themes = len(Themes)
|
stats.Themes = len(Themes)
|
||||||
stats.Reports = 0 // TODO: Do the report count. Only show open threads?
|
stats.Reports = 0 // TODO: Do the report count. Only show open threads?
|
||||||
|
@ -160,12 +161,12 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
|
||||||
tname = "_" + theme.Name
|
tname = "_" + theme.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
header.AddPreScriptAsync("template_" + name + tname + ".js")
|
h.AddPreScriptAsync("template_" + name + tname + ".js")
|
||||||
}
|
}
|
||||||
addPreScript("alert")
|
addPreScript("alert")
|
||||||
addPreScript("notice")
|
addPreScript("notice")
|
||||||
|
|
||||||
return header, stats, nil
|
return h, stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func simplePanelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, rerr RouteError) {
|
func simplePanelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, rerr RouteError) {
|
||||||
|
|
673
gen_router.go
673
gen_router.go
File diff suppressed because it is too large
Load Diff
|
@ -800,6 +800,7 @@
|
||||||
"panel_menu_stats_topics":"Topics",
|
"panel_menu_stats_topics":"Topics",
|
||||||
"panel_menu_stats_forums":"Forums",
|
"panel_menu_stats_forums":"Forums",
|
||||||
"panel_menu_stats_routes":"Routes",
|
"panel_menu_stats_routes":"Routes",
|
||||||
|
"panel_menu_stats_routes_perf":"Routes Perf",
|
||||||
"panel_menu_stats_agents":"Agents",
|
"panel_menu_stats_agents":"Agents",
|
||||||
"panel_menu_stats_systems":"Systems",
|
"panel_menu_stats_systems":"Systems",
|
||||||
"panel_menu_stats_languages":"Languages",
|
"panel_menu_stats_languages":"Languages",
|
||||||
|
|
|
@ -46,6 +46,8 @@ func TestProcessWhere(t *testing.T) {
|
||||||
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenNumber, "0"})
|
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenNumber, "0"})
|
||||||
whs = processWhere("uid=20")
|
whs = processWhere("uid=20")
|
||||||
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenNumber, "20"})
|
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenNumber, "20"})
|
||||||
|
whs = processWhere("uid=uid+1")
|
||||||
|
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenColumn, "uid"}, MT{TokenOp, "+"}, MT{TokenNumber, "1"})
|
||||||
whs = processWhere("uid='1'")
|
whs = processWhere("uid='1'")
|
||||||
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenString, "1"})
|
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenString, "1"})
|
||||||
whs = processWhere("uid='t'")
|
whs = processWhere("uid='t'")
|
||||||
|
|
|
@ -243,6 +243,7 @@ func panelRoutes() *RouteGroup {
|
||||||
|
|
||||||
View("panel.AnalyticsViews", "/panel/analytics/views/").Before("ParseForm"),
|
View("panel.AnalyticsViews", "/panel/analytics/views/").Before("ParseForm"),
|
||||||
View("panel.AnalyticsRoutes", "/panel/analytics/routes/").Before("ParseForm"),
|
View("panel.AnalyticsRoutes", "/panel/analytics/routes/").Before("ParseForm"),
|
||||||
|
View("panel.AnalyticsRoutesPerf", "/panel/analytics/routes-perf/").Before("ParseForm"),
|
||||||
View("panel.AnalyticsAgents", "/panel/analytics/agents/").Before("ParseForm"),
|
View("panel.AnalyticsAgents", "/panel/analytics/agents/").Before("ParseForm"),
|
||||||
View("panel.AnalyticsSystems", "/panel/analytics/systems/").Before("ParseForm"),
|
View("panel.AnalyticsSystems", "/panel/analytics/systems/").Before("ParseForm"),
|
||||||
View("panel.AnalyticsLanguages", "/panel/analytics/langs/").Before("ParseForm"),
|
View("panel.AnalyticsLanguages", "/panel/analytics/langs/").Before("ParseForm"),
|
||||||
|
|
|
@ -81,6 +81,7 @@ func analyticsTimeRange(rawTimeRange string) (*AnalyticsTimeRange, error) {
|
||||||
return tRange, nil
|
return tRange, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Clamp it rather than using an offset off the current time to avoid chaotic changes in stats as adjacent sets converge and diverge?
|
||||||
func analyticsTimeRangeToLabelList(timeRange *AnalyticsTimeRange) (revLabelList []int64, labelList []int64, viewMap map[int64]int64) {
|
func analyticsTimeRangeToLabelList(timeRange *AnalyticsTimeRange) (revLabelList []int64, labelList []int64, viewMap map[int64]int64) {
|
||||||
viewMap = make(map[int64]int64)
|
viewMap = make(map[int64]int64)
|
||||||
currentTime := time.Now().Unix()
|
currentTime := time.Now().Unix()
|
||||||
|
@ -715,6 +716,171 @@ func AnalyticsPerf(w http.ResponseWriter, r *http.Request, user c.User) c.RouteE
|
||||||
return renderTemplate("panel", w, r, basePage.Header, c.Panel{basePage, "panel_analytics_right", "analytics", "panel_analytics_performance", pi})
|
return renderTemplate("panel", w, r, basePage.Header, c.Panel{basePage, "panel_analytics_right", "analytics", "panel_analytics_performance", pi})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func analyticsRowsToAvgDuoMap(rows *sql.Rows, labelList []int64, avgMap map[int64]int64) (map[string]map[int64]int64, map[string]int, error) {
|
||||||
|
aMap := make(map[string]map[int64]int64)
|
||||||
|
nameMap := make(map[string]int)
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var count int64
|
||||||
|
var name string
|
||||||
|
var createdAt time.Time
|
||||||
|
err := rows.Scan(&count, &name, &createdAt)
|
||||||
|
if err != nil {
|
||||||
|
return aMap, nameMap, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Bulk log this
|
||||||
|
unixCreatedAt := createdAt.Unix()
|
||||||
|
if c.Dev.SuperDebug {
|
||||||
|
log.Print("count: ", count)
|
||||||
|
log.Print("name: ", name)
|
||||||
|
log.Print("createdAt: ", createdAt)
|
||||||
|
log.Print("unixCreatedAt: ", unixCreatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
vvMap, ok := aMap[name]
|
||||||
|
if !ok {
|
||||||
|
vvMap = make(map[int64]int64)
|
||||||
|
for key, val := range avgMap {
|
||||||
|
vvMap[key] = val
|
||||||
|
}
|
||||||
|
aMap[name] = vvMap
|
||||||
|
}
|
||||||
|
for _, value := range labelList {
|
||||||
|
if unixCreatedAt > value {
|
||||||
|
vvMap[value] = (vvMap[value] + count) / 2
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nameMap[name] = (nameMap[name] + int(count)) / 2
|
||||||
|
}
|
||||||
|
return aMap, nameMap, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortOVList(ovList []OVItem) (tOVList []OVItem) {
|
||||||
|
// Use bubble sort for now as there shouldn't be too many items
|
||||||
|
for i := 0; i < len(ovList)-1; i++ {
|
||||||
|
for j := 0; j < len(ovList)-1; j++ {
|
||||||
|
if ovList[j].count > ovList[j+1].count {
|
||||||
|
temp := ovList[j]
|
||||||
|
ovList[j] = ovList[j+1]
|
||||||
|
ovList[j+1] = temp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invert the direction
|
||||||
|
for i := len(ovList) - 1; i >= 0; i-- {
|
||||||
|
tOVList = append(tOVList, ovList[i])
|
||||||
|
}
|
||||||
|
return tOVList
|
||||||
|
}
|
||||||
|
|
||||||
|
func analyticsAMapToOVList(aMap map[string]map[int64]int64) (ovList []OVItem) {
|
||||||
|
// Order the map
|
||||||
|
for name, avgMap := range aMap {
|
||||||
|
var totcount int
|
||||||
|
for _, count := range avgMap {
|
||||||
|
totcount = (totcount + int(count)) / 2
|
||||||
|
}
|
||||||
|
ovList = append(ovList, OVItem{name, totcount, avgMap})
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortOVList(ovList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnalyticsRoutesPerf(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
|
||||||
|
basePage, ferr := PreAnalyticsDetail(w, r, &user)
|
||||||
|
if ferr != nil {
|
||||||
|
return ferr
|
||||||
|
}
|
||||||
|
basePage.AddScript("chartist/chartist-plugin-legend.min.js")
|
||||||
|
basePage.AddSheet("chartist/chartist-plugin-legend.css")
|
||||||
|
|
||||||
|
timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
|
||||||
|
if err != nil {
|
||||||
|
return c.LocalError(err.Error(), w, r, user)
|
||||||
|
}
|
||||||
|
// avgMap contains timestamps but not the averages for those stamps
|
||||||
|
revLabelList, labelList, avgMap := analyticsTimeRangeToLabelList(timeRange)
|
||||||
|
|
||||||
|
rows, err := qgen.NewAcc().Select("viewchunks").Columns("avg,route,createdAt").Where("count!=0 AND route!=''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query()
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return c.InternalError(err, w, r)
|
||||||
|
}
|
||||||
|
aMap, routeMap, err := analyticsRowsToAvgDuoMap(rows, labelList, avgMap)
|
||||||
|
if err != nil {
|
||||||
|
return c.InternalError(err, w, r)
|
||||||
|
}
|
||||||
|
//c.DebugLogf("aMap: %+v\n", aMap)
|
||||||
|
//c.DebugLogf("routeMap: %+v\n", routeMap)
|
||||||
|
ovList := analyticsAMapToOVList(aMap)
|
||||||
|
//c.DebugLogf("ovList: %+v\n", ovList)
|
||||||
|
|
||||||
|
ex := strings.Split(r.FormValue("ex"), ",")
|
||||||
|
inEx := func(name string) bool {
|
||||||
|
for _, e := range ex {
|
||||||
|
if e == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// TODO: Adjust for the missing chunks in week and month
|
||||||
|
var avgList []int64
|
||||||
|
var avgItems []c.PanelAnalyticsItemUnit
|
||||||
|
for _, value := range revLabelList {
|
||||||
|
avgList = append(avgList, avgMap[value])
|
||||||
|
cv, cu := c.ConvertPerfUnit(float64(avgMap[value]))
|
||||||
|
avgItems = append(avgItems, c.PanelAnalyticsItemUnit{Time: value, Unit: cu, Count: int64(cv)})
|
||||||
|
}
|
||||||
|
graph := c.PanelTimeGraph{Series: [][]int64{avgList}, Labels: labelList}
|
||||||
|
c.DebugLogf("graph: %+v\n", graph)
|
||||||
|
pi := c.PanelAnalyticsPerf{graph, avgItems, timeRange.Range, timeRange.Unit, "time", typ}
|
||||||
|
return renderTemplate("panel", w, r, basePage.Header, c.Panel{basePage, "panel_analytics_right", "analytics", "panel_analytics_performance", pi})
|
||||||
|
*/
|
||||||
|
|
||||||
|
var vList [][]int64
|
||||||
|
var legendList []string
|
||||||
|
var i int
|
||||||
|
for _, ovitem := range ovList {
|
||||||
|
if inEx(ovitem.name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var viewList []int64
|
||||||
|
for _, value := range revLabelList {
|
||||||
|
viewList = append(viewList, ovitem.viewMap[value])
|
||||||
|
}
|
||||||
|
vList = append(vList, viewList)
|
||||||
|
legendList = append(legendList, ovitem.name)
|
||||||
|
if i >= 6 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
graph := c.PanelTimeGraph{Series: vList, Labels: labelList, Legends: legendList}
|
||||||
|
c.DebugLogf("graph: %+v\n", graph)
|
||||||
|
|
||||||
|
// TODO: Sort this slice
|
||||||
|
var routeItems []c.PanelAnalyticsRoutesPerfItem
|
||||||
|
for route, count := range routeMap {
|
||||||
|
if inEx(route) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cv, cu := c.ConvertPerfUnit(float64(count))
|
||||||
|
routeItems = append(routeItems, c.PanelAnalyticsRoutesPerfItem{
|
||||||
|
Route: route,
|
||||||
|
Unit: cu,
|
||||||
|
Count: int(cv),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pi := c.PanelAnalyticsRoutesPerfPage{basePage, routeItems, graph, timeRange.Range}
|
||||||
|
return renderTemplate("panel", w, r, basePage.Header, c.Panel{basePage, "panel_analytics_right", "analytics", "panel_analytics_routes_perf", pi})
|
||||||
|
}
|
||||||
|
|
||||||
func analyticsRowsToRefMap(rows *sql.Rows) (map[string]int, error) {
|
func analyticsRowsToRefMap(rows *sql.Rows) (map[string]int, error) {
|
||||||
nameMap := make(map[string]int)
|
nameMap := make(map[string]int)
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
@ -792,23 +958,7 @@ func analyticsVMapToOVList(vMap map[string]map[int64]int64) (ovList []OVItem) {
|
||||||
ovList = append(ovList, OVItem{name, totcount, viewMap})
|
ovList = append(ovList, OVItem{name, totcount, viewMap})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use bubble sort for now as there shouldn't be too many items
|
return sortOVList(ovList)
|
||||||
for i := 0; i < len(ovList)-1; i++ {
|
|
||||||
for j := 0; j < len(ovList)-1; j++ {
|
|
||||||
if ovList[j].count > ovList[j+1].count {
|
|
||||||
temp := ovList[j]
|
|
||||||
ovList[j] = ovList[j+1]
|
|
||||||
ovList[j+1] = temp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invert the direction
|
|
||||||
var tOVList []OVItem
|
|
||||||
for i := len(ovList) - 1; i >= 0; i-- {
|
|
||||||
tOVList = append(tOVList, ovList[i])
|
|
||||||
}
|
|
||||||
return tOVList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func AnalyticsForums(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
|
func AnalyticsForums(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
|
||||||
|
@ -917,7 +1067,10 @@ func AnalyticsRoutes(w http.ResponseWriter, r *http.Request, user c.User) c.Rout
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.InternalError(err, w, r)
|
return c.InternalError(err, w, r)
|
||||||
}
|
}
|
||||||
|
//c.DebugLogf("vMap: %+v\n", vMap)
|
||||||
|
//c.DebugLogf("routeMap: %+v\n", routeMap)
|
||||||
ovList := analyticsVMapToOVList(vMap)
|
ovList := analyticsVMapToOVList(vMap)
|
||||||
|
//c.DebugLogf("ovList: %+v\n", ovList)
|
||||||
|
|
||||||
ex := strings.Split(r.FormValue("ex"), ",")
|
ex := strings.Split(r.FormValue("ex"), ",")
|
||||||
inEx := func(name string) bool {
|
inEx := func(name string) bool {
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
<div class="colstack_item colstack_head">
|
||||||
|
<div class="rowitem">
|
||||||
|
<h1>{{lang "panel_stats_routes_head"}}</h1>
|
||||||
|
{{template "panel_analytics_time_range.html" . }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form id="timeRangeForm" name="timeRangeForm" action="/panel/analytics/routes-perf/" method="get"></form>
|
||||||
|
<div id="panel_analytics_routes_chart" class="colstack_graph_holder">
|
||||||
|
<div class="ct_chart"></div>
|
||||||
|
</div>
|
||||||
|
<div id="panel_analytics_routes" class="colstack_item rowlist">
|
||||||
|
{{range .ItemList}}
|
||||||
|
<div class="rowitem panel_compactrow editable_parent">
|
||||||
|
<a href="/panel/analytics/route/{{.Route}}" class="panel_upshift">{{.Route}}</a>
|
||||||
|
<span class="panel_compacttext to_right">{{.Count}}{{.Unit}}</span>
|
||||||
|
</div>
|
||||||
|
{{else}}<div class="rowitem passive rowmsg">{{lang "panel_stats_routes_no_routes"}}</div>{{end}}
|
||||||
|
</div>
|
||||||
|
{{template "panel_analytics_script_perf.html" . }}
|
|
@ -50,6 +50,9 @@
|
||||||
<div class="rowitem passive submenu">
|
<div class="rowitem passive submenu">
|
||||||
<a href="/panel/analytics/routes/">{{lang "panel_menu_stats_routes"}}</a>
|
<a href="/panel/analytics/routes/">{{lang "panel_menu_stats_routes"}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="rowitem passive submenu">
|
||||||
|
<a href="/panel/analytics/routes-perf/">{{lang "panel_menu_stats_routes_perf"}}</a>
|
||||||
|
</div>
|
||||||
<div class="rowitem passive submenu">
|
<div class="rowitem passive submenu">
|
||||||
<a href="/panel/analytics/agents/">{{lang "panel_menu_stats_agents"}}</a>
|
<a href="/panel/analytics/agents/">{{lang "panel_menu_stats_agents"}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue