From 1fb497adf80966e1674caac4d2bb64fd1eaa15b1 Mon Sep 17 00:00:00 2001 From: Azareal Date: Sun, 24 Feb 2019 11:29:06 +1000 Subject: [PATCH] Deployed multi-series charts across the entirety of the analytics panel. Added the one year time range to the analytics panes. Dates are now shown on detail panes for Request, Topic and Post analytics instead of times for higher time ranges. The labels should now show up properly for the three month time range charts. The paginator should now work properly for login logs. Pushed a potential fix for subsequent pages with only one item not showing. up. Executing a search query should now change the title. Fixed a bug where the user agent parser choked on : characters. Fixed the ordering of items in the multi-series charts which caused the most important items to get booted out rather then the least important ones. Tweaked the padding on the User Manager items for Nox so they won't break onto multiple lines so readily. Fixed a potential issue with topic list titles. Fixed a potential crash bug in the Forum Analytics for deleted forums. Added the Count method to LoginLogStore. Continued work on the ElasticSearch mapping setup utility. Added the topic_list.search_head phrase. Added the panel_statistics_time_range_one_year phrase. --- cmd/elasticsearch/setup.go | 110 ++++++--- common/misc_logs.go | 11 + common/pages.go | 4 + common/parser.go | 5 +- gen_router.go | 2 +- install/mysql.go | 1 - langs/english.json | 2 + public/analytics.js | 16 +- public/global.js | 16 +- router_gen/main.go | 2 +- routes/account.go | 2 +- routes/panel/analytics.go | 262 +++++++++++++++------- routes/topic_list.go | 2 +- templates/panel_analytics_forums.html | 4 + templates/panel_analytics_langs.html | 4 + templates/panel_analytics_posts.html | 2 +- templates/panel_analytics_routes.html | 4 + templates/panel_analytics_script.html | 2 +- templates/panel_analytics_systems.html | 4 + templates/panel_analytics_time_range.html | 1 + templates/panel_analytics_topics.html | 2 +- templates/panel_analytics_views.html | 2 +- themes/nox/public/panel.css | 4 + 23 files changed, 350 insertions(+), 114 deletions(-) diff --git a/cmd/elasticsearch/setup.go b/cmd/elasticsearch/setup.go index 24cf9248..f2a874ae 100644 --- a/cmd/elasticsearch/setup.go +++ b/cmd/elasticsearch/setup.go @@ -175,32 +175,92 @@ type ESReply struct { } func setupData(client *elastic.Client) error { - err := qgen.NewAcc().Select("topics").Cols("tid, title, content, createdBy, ipaddress").Each(func(rows *sql.Rows) error { - var tid, createdBy int - var title, content, ip string - err := rows.Scan(&tid, &title, &content, &createdBy, &ip) - if err != nil { - return err + tcount := 4 + errs := make(chan error) + + go func() { + tin := make([]chan ESTopic, tcount) + tf := func(tin chan ESTopic) { + for { + topic, more := <-tin + if !more { + break + } + _, err := client.Index().Index("topics").Type("_doc").Id(strconv.Itoa(topic.ID)).BodyJson(topic).Do(context.Background()) + if err != nil { + errs <- err + } + } + } + for i := 0; i < 4; i++ { + go tf(tin[i]) } - topic := ESTopic{tid, title, content, createdBy, ip} - _, err = client.Index().Index("topics").Type("_doc").Id(strconv.Itoa(tid)).BodyJson(topic).Do(context.Background()) - return err - }) - if err != nil { - return err + oi := 0 + err := qgen.NewAcc().Select("topics").Cols("tid, title, content, createdBy, ipaddress").Each(func(rows *sql.Rows) error { + topic := ESTopic{} + err := rows.Scan(&topic.ID, &topic.Title, &topic.Content, &topic.CreatedBy, &topic.IPAddress) + if err != nil { + return err + } + tin[oi] <- topic + if oi < 3 { + oi++ + } + return nil + }) + for i := 0; i < 4; i++ { + close(tin[i]) + } + errs <- err + }() + + go func() { + rin := make([]chan ESReply, tcount) + rf := func(rin chan ESReply) { + for { + reply, more := <-rin + if !more { + break + } + _, err := client.Index().Index("replies").Type("_doc").Id(strconv.Itoa(reply.ID)).BodyJson(reply).Do(context.Background()) + if err != nil { + errs <- err + } + } + } + for i := 0; i < 4; i++ { + rf(rin[i]) + } + oi := 0 + err := qgen.NewAcc().Select("replies").Cols("rid, tid, content, createdBy, ipaddress").Each(func(rows *sql.Rows) error { + reply := ESReply{} + err := rows.Scan(&reply.ID, &reply.TID, &reply.Content, &reply.CreatedBy, &reply.IPAddress) + if err != nil { + return err + } + rin[oi] <- reply + if oi < 3 { + oi++ + } + return nil + }) + for i := 0; i < 4; i++ { + close(rin[i]) + } + errs <- err + }() + + fin := 0 + for { + err := <-errs + if err == nil { + fin++ + if fin == 2 { + return nil + } + } else { + return err + } } - - return qgen.NewAcc().Select("replies").Cols("rid, tid, content, createdBy, ipaddress").Each(func(rows *sql.Rows) error { - var rid, tid, createdBy int - var content, ip string - err := rows.Scan(&rid, &tid, &content, &createdBy, &ip) - if err != nil { - return err - } - - reply := ESReply{rid, tid, content, createdBy, ip} - _, err = client.Index().Index("replies").Type("_doc").Id(strconv.Itoa(rid)).BodyJson(reply).Do(context.Background()) - return err - }) } diff --git a/common/misc_logs.go b/common/misc_logs.go index a76a5ff2..ea67ceb9 100644 --- a/common/misc_logs.go +++ b/common/misc_logs.go @@ -143,17 +143,20 @@ func (log *LoginLogItem) Create() (id int, err error) { type LoginLogStore interface { GlobalCount() (logCount int) + Count(uid int) (logCount int) GetOffset(uid int, offset int, perPage int) (logs []LoginLogItem, err error) } type SQLLoginLogStore struct { count *sql.Stmt + countForUser *sql.Stmt getOffsetByUser *sql.Stmt } func NewLoginLogStore(acc *qgen.Accumulator) (*SQLLoginLogStore, error) { return &SQLLoginLogStore{ count: acc.Count("login_logs").Prepare(), + countForUser: acc.Count("login_logs").Where("uid = ?").Prepare(), getOffsetByUser: acc.Select("login_logs").Columns("lid, success, ipaddress, doneAt").Where("uid = ?").Orderby("doneAt DESC").Limit("?,?").Prepare(), }, acc.FirstError() } @@ -166,6 +169,14 @@ func (store *SQLLoginLogStore) GlobalCount() (logCount int) { return logCount } +func (store *SQLLoginLogStore) Count(uid int) (logCount int) { + err := store.countForUser.QueryRow(uid).Scan(&logCount) + if err != nil { + LogError(err) + } + return logCount +} + func (store *SQLLoginLogStore) GetOffset(uid int, offset int, perPage int) (logs []LoginLogItem, err error) { rows, err := store.getOffsetByUser.Query(uid, offset, perPage) if err != nil { diff --git a/common/pages.go b/common/pages.go index c5e99e2f..f5a373e8 100644 --- a/common/pages.go +++ b/common/pages.go @@ -277,6 +277,8 @@ type PanelAnalyticsPage struct { Graph PanelTimeGraph ViewItems []PanelAnalyticsItem TimeRange string + Unit string + TimeType string } type PanelAnalyticsRoutesItem struct { @@ -287,9 +289,11 @@ type PanelAnalyticsRoutesItem struct { type PanelAnalyticsRoutesPage struct { *BasePanelPage ItemList []PanelAnalyticsRoutesItem + Graph PanelTimeGraph TimeRange string } +// TODO: Rename the fields as this structure is being used in a generic way now type PanelAnalyticsAgentsItem struct { Agent string FriendlyAgent string diff --git a/common/parser.go b/common/parser.go index 527de1f0..ac0469e5 100644 --- a/common/parser.go +++ b/common/parser.go @@ -880,10 +880,11 @@ func PageOffset(count int, page int, perPage int) (int, int, int) { page = 1 } + // ? - This has been commented out as it created a bug in the user manager where the first user on a page wouldn't be accessible // We don't want the offset to overflow the slices, if everything's in memory - if offset >= (count - 1) { + /*if offset >= (count - 1) { offset = 0 - } + }*/ return offset, page, lastPage } diff --git a/gen_router.go b/gen_router.go index 1cc02f7e..38571705 100644 --- a/gen_router.go +++ b/gen_router.go @@ -768,7 +768,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { for _, item := range StringToBytes(ua) { if (item > 64 && item < 91) || (item > 96 && item < 123) { buffer = append(buffer, item) - } else if item == ' ' || item == '(' || item == ')' || item == '-' || (item > 47 && item < 58) || item == '_' || item == ';' || item == '.' || item == '+' || item == '~' || (item == ':' && bytes.Equal(buffer,[]byte("http"))) || item == ',' || item == '/' { + } else if item == ' ' || item == '(' || item == ')' || item == '-' || (item > 47 && item < 58) || item == '_' || item == ';' || item == ':' || item == '.' || item == '+' || item == '~' || (item == ':' && bytes.Equal(buffer,[]byte("http"))) || item == ',' || item == '/' { if len(buffer) != 0 { if len(buffer) > 2 { // Use an unsafe zero copy conversion here just to use the switch, it's not safe for this string to escape from here, as it will get mutated, so do a regular string conversion in append diff --git a/install/mysql.go b/install/mysql.go index 42a16a26..6089a51a 100644 --- a/install/mysql.go +++ b/install/mysql.go @@ -137,7 +137,6 @@ func (ins *MysqlInstaller) TableDefs() (err error) { _, err = ins.db.Exec(string(data)) if err != nil { fmt.Println("Failed query:", string(data)) - panic("Failed query: " + string(data)) return err } } diff --git a/langs/english.json b/langs/english.json index 1f0dabc1..76219164 100644 --- a/langs/english.json +++ b/langs/english.json @@ -518,6 +518,7 @@ "quick_topic.add_file_button":"Add File", "quick_topic.cancel_button":"Cancel", + "topic_list.search_head":"Search Results", "topic_list.create_topic_tooltip":"Create Topic", "topic_list.create_topic_aria":"Create a topic", "topic_list.moderate":"Moderate", @@ -831,6 +832,7 @@ "panel_statistics_topic_counts_head":"Topic Counts", "panel_statistics_requests_head":"Requests", + "panel_statistics_time_range_one_year":"1 year", "panel_statistics_time_range_three_months":"3 months", "panel_statistics_time_range_one_month":"1 month", "panel_statistics_time_range_one_week":"1 week", diff --git a/public/analytics.js b/public/analytics.js index 71c1d80b..b26b2711 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -6,7 +6,21 @@ // TODO: Load rawLabels and seriesData dynamically rather than potentially fiddling with nonces for the CSP? function buildStatsChart(rawLabels, seriesData, timeRange, legendNames) { let labels = []; - if(timeRange=="one-month") { + if(timeRange=="one-year") { + labels = ["today","01 months"]; + for(let i = 2; i < 12; i++) { + let label = "0" + i + " months"; + if(label.length > "01 months".length) label = label.substr(1); + labels.push(label); + } + } else if(timeRange=="three-months") { + labels = ["today","01 days"]; + for(let i = 2; i < 90; i = i + 3) { + let label = "0" + i + " days"; + if(label.length > "01 days".length) label = label.substr(1); + labels.push(label); + } + } else if(timeRange=="one-month") { labels = ["today","01 days"]; for(let i = 2; i < 30; i++) { let label = "0" + i + " days"; diff --git a/public/global.js b/public/global.js index 69c752c8..0e1030fb 100644 --- a/public/global.js +++ b/public/global.js @@ -234,7 +234,6 @@ function runWebSockets() { console.log("empty topic list"); return; } - // TODO: Fix the data race where the function hasn't been loaded yet let renTopic = Template_topics_topic(topic); $(".topic_row[data-tid='"+topic.ID+"']").addClass("ajax_topic_dupe"); @@ -318,7 +317,7 @@ function PageOffset(count, page, perPage) { } // We don't want the offset to overflow the slices, if everything's in memory - if(offset >= (count - 1)) offset = 0; + //if(offset >= (count - 1)) offset = 0; return {Offset:offset, Page:page, LastPage:lastPage} } function LastPage(count, perPage) { @@ -517,6 +516,8 @@ function mainInit(){ for(let i = 0; i < topics.length;i++) out += Template_topics_topic(topics[i]); $(".topic_list").html(out); + document.title = phraseBox["topic_list"]["topic_list.search_head"]; + $(".topic_list_title h1").text(phraseBox["topic_list"]["topic_list.search_head"]); let obj = {Title: document.title, Url: url+q}; history.pushState(obj, obj.Title, obj.Url); rebuildPaginator(data.LastPage); @@ -1046,6 +1047,17 @@ function mainInit(){ this.innerText = formattedTime; }); + $(".unix_to_date").each(function(){ + // TODO: Localise this + let monthList = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + let date = new Date(this.innerText * 1000); + console.log("date: ", date); + let day = "0" + date.getDate(); + let formattedTime = monthList[date.getMonth()] + " " + day.substr(-2) + " " + date.getFullYear(); + console.log("formattedTime:", formattedTime); + this.innerText = formattedTime; + }); + this.onkeyup = function(event) { if(event.which == 37) this.querySelectorAll("#prevFloat a")[0].click(); if(event.which == 39) this.querySelectorAll("#nextFloat a")[0].click(); diff --git a/router_gen/main.go b/router_gen/main.go index df2bc689..571e8ec2 100644 --- a/router_gen/main.go +++ b/router_gen/main.go @@ -560,7 +560,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { for _, item := range StringToBytes(ua) { if (item > 64 && item < 91) || (item > 96 && item < 123) { buffer = append(buffer, item) - } else if item == ' ' || item == '(' || item == ')' || item == '-' || (item > 47 && item < 58) || item == '_' || item == ';' || item == '.' || item == '+' || item == '~' || (item == ':' && bytes.Equal(buffer,[]byte("http"))) || item == ',' || item == '/' { + } else if item == ' ' || item == '(' || item == ')' || item == '-' || (item > 47 && item < 58) || item == '_' || item == ';' || item == ':' || item == '.' || item == '+' || item == '~' || (item == ':' && bytes.Equal(buffer,[]byte("http"))) || item == ',' || item == '/' { if len(buffer) != 0 { if len(buffer) > 2 { // Use an unsafe zero copy conversion here just to use the switch, it's not safe for this string to escape from here, as it will get mutated, so do a regular string conversion in append diff --git a/routes/account.go b/routes/account.go index 6935466c..acf1ca10 100644 --- a/routes/account.go +++ b/routes/account.go @@ -705,7 +705,7 @@ func AccountEditEmailTokenSubmit(w http.ResponseWriter, r *http.Request, user co func AccountLogins(w http.ResponseWriter, r *http.Request, user common.User, header *common.Header) common.RouteError { accountEditHead("account_logins", w, r, &user, header) - logCount := common.LoginLogs.GlobalCount() + logCount := common.LoginLogs.Count(user.ID) page, _ := strconv.Atoi(r.FormValue("page")) perPage := 12 offset, page, lastPage := common.PageOffset(logCount, page, perPage) diff --git a/routes/panel/analytics.go b/routes/panel/analytics.go index 73b92594..49282721 100644 --- a/routes/panel/analytics.go +++ b/routes/panel/analytics.go @@ -31,6 +31,12 @@ func analyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange, err switch rawTimeRange { // This might be pushing it, we might want to come up with a more efficient scheme for dealing with large timeframes like this + case "one-year": + timeRange.Quantity = 12 + timeRange.Unit = "month" + timeRange.Slices = 12 + timeRange.SliceWidth = 60 * 60 * 24 * 30 + timeRange.Range = "one-year" case "three-months": timeRange.Quantity = 90 timeRange.Unit = "day" @@ -153,8 +159,11 @@ func AnalyticsViews(w http.ResponseWriter, r *http.Request, user common.User) co } graph := common.PanelTimeGraph{Series: [][]int64{viewList}, Labels: labelList} common.DebugLogf("graph: %+v\n", graph) - - pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range} + var ttime string + if timeRange.Range == "six-hours" || timeRange.Range == "twelve-hours" || timeRange.Range == "one-day" { + ttime = "time" + } + pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range, timeRange.Unit, ttime} return renderTemplate("panel_analytics_views", w, r, basePage.Header, &pi) } @@ -416,7 +425,7 @@ func AnalyticsTopics(w http.ResponseWriter, r *http.Request, user common.User) c } graph := common.PanelTimeGraph{Series: [][]int64{viewList}, Labels: labelList} common.DebugLogf("graph: %+v\n", graph) - pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range} + pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range, timeRange.Unit, "time"} return renderTemplate("panel_analytics_topics", w, r, basePage.Header, &pi) } @@ -449,37 +458,10 @@ func AnalyticsPosts(w http.ResponseWriter, r *http.Request, user common.User) co } graph := common.PanelTimeGraph{Series: [][]int64{viewList}, Labels: labelList} common.DebugLogf("graph: %+v\n", graph) - pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range} + pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range, timeRange.Unit, "time"} return renderTemplate("panel_analytics_posts", w, r, basePage.Header, &pi) } -/*func analyticsRowsToViewMap(rows *sql.Rows, labelList []int64, viewMap map[int64]int64) (map[int64]int64, error) { - defer rows.Close() - for rows.Next() { - var count int64 - var createdAt time.Time - err := rows.Scan(&count, &createdAt) - if err != nil { - return viewMap, err - } - - var unixCreatedAt = createdAt.Unix() - // TODO: Bulk log this - if common.Dev.SuperDebug { - log.Print("count: ", count) - log.Print("createdAt: ", createdAt) - log.Print("unixCreatedAt: ", unixCreatedAt) - } - for _, value := range labelList { - if unixCreatedAt > value { - viewMap[value] += count - break - } - } - } - return viewMap, rows.Err() -}*/ - func analyticsRowsToNameMap(rows *sql.Rows) (map[string]int, error) { nameMap := make(map[string]int) defer rows.Close() @@ -540,24 +522,96 @@ func analyticsRowsToDuoMap(rows *sql.Rows, labelList []int64, viewMap map[int64] return vMap, nameMap, rows.Err() } +type OVItem struct { + name string + count int + viewMap map[int64]int64 +} + +func analyticsVMapToOVList(vMap map[string]map[int64]int64) (ovList []OVItem) { + // Order the map + for name, viewMap := range vMap { + var totcount int + for _, count := range viewMap { + totcount += int(count) + } + ovList = append(ovList, OVItem{name, totcount, viewMap}) + } + + // 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 + 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 common.User) common.RouteError { - basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics") + 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 common.LocalError(err.Error(), w, r, user) } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) - rows, err := qgen.NewAcc().Select("viewchunks_forums").Columns("count, forum").Where("forum != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + rows, err := qgen.NewAcc().Select("viewchunks_forums").Columns("count, forum, createdAt").Where("forum != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } - forumMap, err := analyticsRowsToNameMap(rows) + vMap, forumMap, err := analyticsRowsToDuoMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } + ovList := analyticsVMapToOVList(vMap) + + var vList [][]int64 + var legendList []string + var i int + for _, ovitem := range ovList { + var viewList []int64 + for _, value := range revLabelList { + viewList = append(viewList, ovitem.viewMap[value]) + } + vList = append(vList, viewList) + fid, err := strconv.Atoi(ovitem.name) + if err != nil { + return common.InternalError(err, w, r) + } + var lName string + forum, err := common.Forums.Get(fid) + if err == sql.ErrNoRows { + // TODO: Localise this + lName = "Deleted Forum" + } else if err != nil { + return common.InternalError(err, w, r) + } else { + lName = forum.Name + } + legendList = append(legendList, lName) + if i >= 6 { + break + } + i++ + } + graph := common.PanelTimeGraph{Series: vList, Labels: labelList, Legends: legendList} + common.DebugLogf("graph: %+v\n", graph) // TODO: Sort this slice var forumItems []common.PanelAnalyticsAgentsItem @@ -566,39 +620,68 @@ func AnalyticsForums(w http.ResponseWriter, r *http.Request, user common.User) c if err != nil { return common.InternalError(err, w, r) } + var lName string forum, err := common.Forums.Get(fid) - if err != nil { + if err == sql.ErrNoRows { + // TODO: Localise this + lName = "Deleted Forum" + } else if err != nil { return common.InternalError(err, w, r) + } else { + lName = forum.Name } forumItems = append(forumItems, common.PanelAnalyticsAgentsItem{ Agent: sfid, - FriendlyAgent: forum.Name, + FriendlyAgent: lName, Count: count, }) } - pi := common.PanelAnalyticsAgentsPage{basePage, forumItems, timeRange.Range} + pi := common.PanelAnalyticsDuoPage{basePage, forumItems, graph, timeRange.Range} return renderTemplate("panel_analytics_forums", w, r, basePage.Header, &pi) } func AnalyticsRoutes(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics") + 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 common.LocalError(err.Error(), w, r, user) } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) - rows, err := qgen.NewAcc().Select("viewchunks").Columns("count, route").Where("route != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + rows, err := qgen.NewAcc().Select("viewchunks").Columns("count, route, createdAt").Where("route != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } - routeMap, err := analyticsRowsToNameMap(rows) + vMap, routeMap, err := analyticsRowsToDuoMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } + ovList := analyticsVMapToOVList(vMap) + + var vList [][]int64 + var legendList []string + var i int + for _, ovitem := range ovList { + 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 := common.PanelTimeGraph{Series: vList, Labels: labelList, Legends: legendList} + common.DebugLogf("graph: %+v\n", graph) // TODO: Sort this slice var routeItems []common.PanelAnalyticsRoutesItem @@ -609,16 +692,10 @@ func AnalyticsRoutes(w http.ResponseWriter, r *http.Request, user common.User) c }) } - pi := common.PanelAnalyticsRoutesPage{basePage, routeItems, timeRange.Range} + pi := common.PanelAnalyticsRoutesPage{basePage, routeItems, graph, timeRange.Range} return renderTemplate("panel_analytics_routes", w, r, basePage.Header, &pi) } -type OVItem struct { - name string - count int - viewMap map[int64]int64 -} - // Trialling multi-series charts func AnalyticsAgents(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { basePage, ferr := PreAnalyticsDetail(w, r, &user) @@ -642,26 +719,7 @@ func AnalyticsAgents(w http.ResponseWriter, r *http.Request, user common.User) c if err != nil { return common.InternalError(err, w, r) } - - // Order the map - var ovList []OVItem - for name, viewMap := range vMap { - var totcount int - for _, count := range viewMap { - totcount += int(count) - } - ovList = append(ovList, OVItem{name, totcount, viewMap}) - } - // 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 - } - } - } + ovList := analyticsVMapToOVList(vMap) var vList [][]int64 var legendList []string @@ -704,23 +762,50 @@ func AnalyticsAgents(w http.ResponseWriter, r *http.Request, user common.User) c } func AnalyticsSystems(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics") + 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 common.LocalError(err.Error(), w, r, user) } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) - rows, err := qgen.NewAcc().Select("viewchunks_systems").Columns("count, system").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + rows, err := qgen.NewAcc().Select("viewchunks_systems").Columns("count, system, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } - osMap, err := analyticsRowsToNameMap(rows) + vMap, osMap, err := analyticsRowsToDuoMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } + ovList := analyticsVMapToOVList(vMap) + + var vList [][]int64 + var legendList []string + var i int + for _, ovitem := range ovList { + var viewList []int64 + for _, value := range revLabelList { + viewList = append(viewList, ovitem.viewMap[value]) + } + vList = append(vList, viewList) + lName, ok := phrases.GetOSPhrase(ovitem.name) + if !ok { + lName = ovitem.name + } + legendList = append(legendList, lName) + if i >= 6 { + break + } + i++ + } + graph := common.PanelTimeGraph{Series: vList, Labels: labelList, Legends: legendList} + common.DebugLogf("graph: %+v\n", graph) // TODO: Sort this slice var systemItems []common.PanelAnalyticsAgentsItem @@ -736,28 +821,55 @@ func AnalyticsSystems(w http.ResponseWriter, r *http.Request, user common.User) }) } - pi := common.PanelAnalyticsAgentsPage{basePage, systemItems, timeRange.Range} + pi := common.PanelAnalyticsDuoPage{basePage, systemItems, graph, timeRange.Range} return renderTemplate("panel_analytics_systems", w, r, basePage.Header, &pi) } func AnalyticsLanguages(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { - basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics") + 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 common.LocalError(err.Error(), w, r, user) } + revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) - rows, err := qgen.NewAcc().Select("viewchunks_langs").Columns("count, lang").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() + rows, err := qgen.NewAcc().Select("viewchunks_langs").Columns("count, lang, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } - langMap, err := analyticsRowsToNameMap(rows) + vMap, langMap, err := analyticsRowsToDuoMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } + ovList := analyticsVMapToOVList(vMap) + + var vList [][]int64 + var legendList []string + var i int + for _, ovitem := range ovList { + var viewList []int64 + for _, value := range revLabelList { + viewList = append(viewList, ovitem.viewMap[value]) + } + vList = append(vList, viewList) + lName, ok := phrases.GetHumanLangPhrase(ovitem.name) + if !ok { + lName = ovitem.name + } + legendList = append(legendList, lName) + if i >= 6 { + break + } + i++ + } + graph := common.PanelTimeGraph{Series: vList, Labels: labelList, Legends: legendList} + common.DebugLogf("graph: %+v\n", graph) // TODO: Can we de-duplicate these analytics functions further? // TODO: Sort this slice @@ -774,7 +886,7 @@ func AnalyticsLanguages(w http.ResponseWriter, r *http.Request, user common.User }) } - pi := common.PanelAnalyticsAgentsPage{basePage, langItems, timeRange.Range} + pi := common.PanelAnalyticsDuoPage{basePage, langItems, graph, timeRange.Range} return renderTemplate("panel_analytics_langs", w, r, basePage.Header, &pi) } diff --git a/routes/topic_list.go b/routes/topic_list.go index 47d60d12..a5c5289c 100644 --- a/routes/topic_list.go +++ b/routes/topic_list.go @@ -29,6 +29,7 @@ func TopicListMostViewed(w http.ResponseWriter, r *http.Request, user common.Use // TODO: Implement search func TopicListCommon(w http.ResponseWriter, r *http.Request, user common.User, header *common.Header, torder string, tsorder string) common.RouteError { + header.Title = phrases.GetTitlePhrase("topics") header.Zone = "topics" header.Path = "/topics/" header.MetaDesc = header.Settings["meta_desc"].(string) @@ -189,7 +190,6 @@ func TopicListCommon(w http.ResponseWriter, r *http.Request, user common.User, h return nil } - header.Title = phrases.GetTitlePhrase("topics") pi := common.TopicListPage{header, topicList, forumList, common.Config.DefaultForum, common.TopicListSort{torder, false}, paginator} return renderTemplate("topics", w, r, header, pi) } diff --git a/templates/panel_analytics_forums.html b/templates/panel_analytics_forums.html index c525cc08..dc0ddf25 100644 --- a/templates/panel_analytics_forums.html +++ b/templates/panel_analytics_forums.html @@ -10,6 +10,9 @@ +
+
+
{{range .ItemList}}
@@ -20,4 +23,5 @@
+{{template "panel_analytics_script.html" . }} {{template "footer.html" . }} diff --git a/templates/panel_analytics_langs.html b/templates/panel_analytics_langs.html index bf221840..6fe220ab 100644 --- a/templates/panel_analytics_langs.html +++ b/templates/panel_analytics_langs.html @@ -10,6 +10,9 @@ +
+
+
{{range .ItemList}}
@@ -20,4 +23,5 @@
+{{template "panel_analytics_script.html" . }} {{template "footer.html" . }} diff --git a/templates/panel_analytics_posts.html b/templates/panel_analytics_posts.html index ae874572..03f1e3fd 100644 --- a/templates/panel_analytics_posts.html +++ b/templates/panel_analytics_posts.html @@ -19,7 +19,7 @@
{{range .ViewItems}}
- {{.Time}} + {{.Time}} {{.Count}}{{lang "panel_statistics_posts_suffix"}}
{{else}}
{{lang "panel_statistics_post_counts_no_post_counts"}}
{{end}} diff --git a/templates/panel_analytics_routes.html b/templates/panel_analytics_routes.html index 09af23b6..5dae4e5a 100644 --- a/templates/panel_analytics_routes.html +++ b/templates/panel_analytics_routes.html @@ -10,6 +10,9 @@
+
+
+
{{range .ItemList}}
@@ -20,4 +23,5 @@
+{{template "panel_analytics_script.html" . }} {{template "footer.html" . }} diff --git a/templates/panel_analytics_script.html b/templates/panel_analytics_script.html index 47243110..1d3cbae2 100644 --- a/templates/panel_analytics_script.html +++ b/templates/panel_analytics_script.html @@ -10,5 +10,5 @@ let seriesData = [{{range .Graph.Series}}[{{range .}} let legendNames = [{{range .Graph.Legends}} {{.}},{{end}} ]; -buildStatsChart(rawLabels, seriesData.reverse(), "{{.TimeRange}}",legendNames.reverse()); +buildStatsChart(rawLabels, seriesData, "{{.TimeRange}}",legendNames); \ No newline at end of file diff --git a/templates/panel_analytics_systems.html b/templates/panel_analytics_systems.html index 54b6b574..12c72856 100644 --- a/templates/panel_analytics_systems.html +++ b/templates/panel_analytics_systems.html @@ -10,6 +10,9 @@ +
+
+
{{range .ItemList}}
@@ -20,4 +23,5 @@
+{{template "panel_analytics_script.html" . }} {{template "footer.html" . }} diff --git a/templates/panel_analytics_time_range.html b/templates/panel_analytics_time_range.html index e688d705..02073190 100644 --- a/templates/panel_analytics_time_range.html +++ b/templates/panel_analytics_time_range.html @@ -1,4 +1,5 @@