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.
This commit is contained in:
Azareal 2019-02-24 11:29:06 +10:00
parent 633c9ef2ec
commit 1fb497adf8
23 changed files with 350 additions and 114 deletions

View File

@ -175,32 +175,92 @@ type ESReply struct {
} }
func setupData(client *elastic.Client) error { func setupData(client *elastic.Client) error {
err := qgen.NewAcc().Select("topics").Cols("tid, title, content, createdBy, ipaddress").Each(func(rows *sql.Rows) error { tcount := 4
var tid, createdBy int errs := make(chan error)
var title, content, ip string
err := rows.Scan(&tid, &title, &content, &createdBy, &ip) go func() {
if err != nil { tin := make([]chan ESTopic, tcount)
return err 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} oi := 0
_, err = client.Index().Index("topics").Type("_doc").Id(strconv.Itoa(tid)).BodyJson(topic).Do(context.Background()) err := qgen.NewAcc().Select("topics").Cols("tid, title, content, createdBy, ipaddress").Each(func(rows *sql.Rows) error {
return err topic := ESTopic{}
}) err := rows.Scan(&topic.ID, &topic.Title, &topic.Content, &topic.CreatedBy, &topic.IPAddress)
if err != nil { if err != nil {
return err 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
})
} }

View File

@ -143,17 +143,20 @@ func (log *LoginLogItem) Create() (id int, err error) {
type LoginLogStore interface { type LoginLogStore interface {
GlobalCount() (logCount int) GlobalCount() (logCount int)
Count(uid int) (logCount int)
GetOffset(uid int, offset int, perPage int) (logs []LoginLogItem, err error) GetOffset(uid int, offset int, perPage int) (logs []LoginLogItem, err error)
} }
type SQLLoginLogStore struct { type SQLLoginLogStore struct {
count *sql.Stmt count *sql.Stmt
countForUser *sql.Stmt
getOffsetByUser *sql.Stmt getOffsetByUser *sql.Stmt
} }
func NewLoginLogStore(acc *qgen.Accumulator) (*SQLLoginLogStore, error) { func NewLoginLogStore(acc *qgen.Accumulator) (*SQLLoginLogStore, error) {
return &SQLLoginLogStore{ return &SQLLoginLogStore{
count: acc.Count("login_logs").Prepare(), 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(), getOffsetByUser: acc.Select("login_logs").Columns("lid, success, ipaddress, doneAt").Where("uid = ?").Orderby("doneAt DESC").Limit("?,?").Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }
@ -166,6 +169,14 @@ func (store *SQLLoginLogStore) GlobalCount() (logCount int) {
return logCount 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) { func (store *SQLLoginLogStore) GetOffset(uid int, offset int, perPage int) (logs []LoginLogItem, err error) {
rows, err := store.getOffsetByUser.Query(uid, offset, perPage) rows, err := store.getOffsetByUser.Query(uid, offset, perPage)
if err != nil { if err != nil {

View File

@ -277,6 +277,8 @@ type PanelAnalyticsPage struct {
Graph PanelTimeGraph Graph PanelTimeGraph
ViewItems []PanelAnalyticsItem ViewItems []PanelAnalyticsItem
TimeRange string TimeRange string
Unit string
TimeType string
} }
type PanelAnalyticsRoutesItem struct { type PanelAnalyticsRoutesItem struct {
@ -287,9 +289,11 @@ type PanelAnalyticsRoutesItem struct {
type PanelAnalyticsRoutesPage struct { type PanelAnalyticsRoutesPage struct {
*BasePanelPage *BasePanelPage
ItemList []PanelAnalyticsRoutesItem ItemList []PanelAnalyticsRoutesItem
Graph PanelTimeGraph
TimeRange string TimeRange string
} }
// 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
FriendlyAgent string FriendlyAgent string

View File

@ -880,10 +880,11 @@ func PageOffset(count int, page int, perPage int) (int, int, int) {
page = 1 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 // 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 offset = 0
} }*/
return offset, page, lastPage return offset, page, lastPage
} }

View File

@ -768,7 +768,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for _, item := range StringToBytes(ua) { for _, item := range StringToBytes(ua) {
if (item > 64 && item < 91) || (item > 96 && item < 123) { if (item > 64 && item < 91) || (item > 96 && item < 123) {
buffer = append(buffer, item) 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) != 0 {
if len(buffer) > 2 { 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 // 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

View File

@ -137,7 +137,6 @@ func (ins *MysqlInstaller) TableDefs() (err error) {
_, err = ins.db.Exec(string(data)) _, err = ins.db.Exec(string(data))
if err != nil { if err != nil {
fmt.Println("Failed query:", string(data)) fmt.Println("Failed query:", string(data))
panic("Failed query: " + string(data))
return err return err
} }
} }

View File

@ -518,6 +518,7 @@
"quick_topic.add_file_button":"Add File", "quick_topic.add_file_button":"Add File",
"quick_topic.cancel_button":"Cancel", "quick_topic.cancel_button":"Cancel",
"topic_list.search_head":"Search Results",
"topic_list.create_topic_tooltip":"Create Topic", "topic_list.create_topic_tooltip":"Create Topic",
"topic_list.create_topic_aria":"Create a topic", "topic_list.create_topic_aria":"Create a topic",
"topic_list.moderate":"Moderate", "topic_list.moderate":"Moderate",
@ -831,6 +832,7 @@
"panel_statistics_topic_counts_head":"Topic Counts", "panel_statistics_topic_counts_head":"Topic Counts",
"panel_statistics_requests_head":"Requests", "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_three_months":"3 months",
"panel_statistics_time_range_one_month":"1 month", "panel_statistics_time_range_one_month":"1 month",
"panel_statistics_time_range_one_week":"1 week", "panel_statistics_time_range_one_week":"1 week",

View File

@ -6,7 +6,21 @@
// TODO: Load rawLabels and seriesData dynamically rather than potentially fiddling with nonces for the CSP? // TODO: Load rawLabels and seriesData dynamically rather than potentially fiddling with nonces for the CSP?
function buildStatsChart(rawLabels, seriesData, timeRange, legendNames) { function buildStatsChart(rawLabels, seriesData, timeRange, legendNames) {
let labels = []; 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"]; labels = ["today","01 days"];
for(let i = 2; i < 30; i++) { for(let i = 2; i < 30; i++) {
let label = "0" + i + " days"; let label = "0" + i + " days";

View File

@ -234,7 +234,6 @@ function runWebSockets() {
console.log("empty topic list"); console.log("empty topic list");
return; return;
} }
// TODO: Fix the data race where the function hasn't been loaded yet // TODO: Fix the data race where the function hasn't been loaded yet
let renTopic = Template_topics_topic(topic); let renTopic = Template_topics_topic(topic);
$(".topic_row[data-tid='"+topic.ID+"']").addClass("ajax_topic_dupe"); $(".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 // 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} return {Offset:offset, Page:page, LastPage:lastPage}
} }
function LastPage(count, perPage) { function LastPage(count, perPage) {
@ -517,6 +516,8 @@ function mainInit(){
for(let i = 0; i < topics.length;i++) out += Template_topics_topic(topics[i]); for(let i = 0; i < topics.length;i++) out += Template_topics_topic(topics[i]);
$(".topic_list").html(out); $(".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}; let obj = {Title: document.title, Url: url+q};
history.pushState(obj, obj.Title, obj.Url); history.pushState(obj, obj.Title, obj.Url);
rebuildPaginator(data.LastPage); rebuildPaginator(data.LastPage);
@ -1046,6 +1047,17 @@ function mainInit(){
this.innerText = formattedTime; 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) { this.onkeyup = function(event) {
if(event.which == 37) this.querySelectorAll("#prevFloat a")[0].click(); if(event.which == 37) this.querySelectorAll("#prevFloat a")[0].click();
if(event.which == 39) this.querySelectorAll("#nextFloat a")[0].click(); if(event.which == 39) this.querySelectorAll("#nextFloat a")[0].click();

View File

@ -560,7 +560,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for _, item := range StringToBytes(ua) { for _, item := range StringToBytes(ua) {
if (item > 64 && item < 91) || (item > 96 && item < 123) { if (item > 64 && item < 91) || (item > 96 && item < 123) {
buffer = append(buffer, item) 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) != 0 {
if len(buffer) > 2 { 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 // 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

View File

@ -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 { func AccountLogins(w http.ResponseWriter, r *http.Request, user common.User, header *common.Header) common.RouteError {
accountEditHead("account_logins", w, r, &user, header) accountEditHead("account_logins", w, r, &user, header)
logCount := common.LoginLogs.GlobalCount() logCount := common.LoginLogs.Count(user.ID)
page, _ := strconv.Atoi(r.FormValue("page")) page, _ := strconv.Atoi(r.FormValue("page"))
perPage := 12 perPage := 12
offset, page, lastPage := common.PageOffset(logCount, page, perPage) offset, page, lastPage := common.PageOffset(logCount, page, perPage)

View File

@ -31,6 +31,12 @@ func analyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange, err
switch rawTimeRange { 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 // 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": case "three-months":
timeRange.Quantity = 90 timeRange.Quantity = 90
timeRange.Unit = "day" 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} graph := common.PanelTimeGraph{Series: [][]int64{viewList}, Labels: labelList}
common.DebugLogf("graph: %+v\n", graph) common.DebugLogf("graph: %+v\n", graph)
var ttime string
pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range} 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) 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} graph := common.PanelTimeGraph{Series: [][]int64{viewList}, Labels: labelList}
common.DebugLogf("graph: %+v\n", graph) 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) 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} graph := common.PanelTimeGraph{Series: [][]int64{viewList}, Labels: labelList}
common.DebugLogf("graph: %+v\n", graph) 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) 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) { func analyticsRowsToNameMap(rows *sql.Rows) (map[string]int, error) {
nameMap := make(map[string]int) nameMap := make(map[string]int)
defer rows.Close() defer rows.Close()
@ -540,24 +522,96 @@ func analyticsRowsToDuoMap(rows *sql.Rows, labelList []int64, viewMap map[int64]
return vMap, nameMap, rows.Err() 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 { 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 { if ferr != nil {
return ferr return ferr
} }
basePage.AddScript("chartist/chartist-plugin-legend.min.js")
basePage.AddSheet("chartist/chartist-plugin-legend.css")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil { if err != nil {
return common.LocalError(err.Error(), w, r, user) 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 { if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
forumMap, err := analyticsRowsToNameMap(rows) vMap, forumMap, err := analyticsRowsToDuoMap(rows, labelList, viewMap)
if err != nil { if err != nil {
return common.InternalError(err, w, r) 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 // TODO: Sort this slice
var forumItems []common.PanelAnalyticsAgentsItem var forumItems []common.PanelAnalyticsAgentsItem
@ -566,39 +620,68 @@ func AnalyticsForums(w http.ResponseWriter, r *http.Request, user common.User) c
if err != nil { if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
var lName string
forum, err := common.Forums.Get(fid) 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) return common.InternalError(err, w, r)
} else {
lName = forum.Name
} }
forumItems = append(forumItems, common.PanelAnalyticsAgentsItem{ forumItems = append(forumItems, common.PanelAnalyticsAgentsItem{
Agent: sfid, Agent: sfid,
FriendlyAgent: forum.Name, FriendlyAgent: lName,
Count: count, 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) return renderTemplate("panel_analytics_forums", w, r, basePage.Header, &pi)
} }
func AnalyticsRoutes(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { 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 { if ferr != nil {
return ferr return ferr
} }
basePage.AddScript("chartist/chartist-plugin-legend.min.js")
basePage.AddSheet("chartist/chartist-plugin-legend.css")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil { if err != nil {
return common.LocalError(err.Error(), w, r, user) 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 { if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
routeMap, err := analyticsRowsToNameMap(rows) vMap, routeMap, err := analyticsRowsToDuoMap(rows, labelList, viewMap)
if err != nil { if err != nil {
return common.InternalError(err, w, r) 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 // TODO: Sort this slice
var routeItems []common.PanelAnalyticsRoutesItem 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) 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 // Trialling multi-series charts
func AnalyticsAgents(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { func AnalyticsAgents(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
basePage, ferr := PreAnalyticsDetail(w, r, &user) 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 { if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
ovList := analyticsVMapToOVList(vMap)
// 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
}
}
}
var vList [][]int64 var vList [][]int64
var legendList []string 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 { 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 { if ferr != nil {
return ferr return ferr
} }
basePage.AddScript("chartist/chartist-plugin-legend.min.js")
basePage.AddSheet("chartist/chartist-plugin-legend.css")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil { if err != nil {
return common.LocalError(err.Error(), w, r, user) 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 { if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
osMap, err := analyticsRowsToNameMap(rows) vMap, osMap, err := analyticsRowsToDuoMap(rows, labelList, viewMap)
if err != nil { if err != nil {
return common.InternalError(err, w, r) 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 // TODO: Sort this slice
var systemItems []common.PanelAnalyticsAgentsItem 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) return renderTemplate("panel_analytics_systems", w, r, basePage.Header, &pi)
} }
func AnalyticsLanguages(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { 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 { if ferr != nil {
return ferr return ferr
} }
basePage.AddScript("chartist/chartist-plugin-legend.min.js")
basePage.AddSheet("chartist/chartist-plugin-legend.css")
timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) timeRange, err := analyticsTimeRange(r.FormValue("timeRange"))
if err != nil { if err != nil {
return common.LocalError(err.Error(), w, r, user) 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 { if err != nil && err != sql.ErrNoRows {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }
langMap, err := analyticsRowsToNameMap(rows) vMap, langMap, err := analyticsRowsToDuoMap(rows, labelList, viewMap)
if err != nil { if err != nil {
return common.InternalError(err, w, r) 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: Can we de-duplicate these analytics functions further?
// TODO: Sort this slice // 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) return renderTemplate("panel_analytics_langs", w, r, basePage.Header, &pi)
} }

View File

@ -29,6 +29,7 @@ func TopicListMostViewed(w http.ResponseWriter, r *http.Request, user common.Use
// TODO: Implement search // TODO: Implement search
func TopicListCommon(w http.ResponseWriter, r *http.Request, user common.User, header *common.Header, torder string, tsorder string) common.RouteError { 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.Zone = "topics"
header.Path = "/topics/" header.Path = "/topics/"
header.MetaDesc = header.Settings["meta_desc"].(string) 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 return nil
} }
header.Title = phrases.GetTitlePhrase("topics")
pi := common.TopicListPage{header, topicList, forumList, common.Config.DefaultForum, common.TopicListSort{torder, false}, paginator} pi := common.TopicListPage{header, topicList, forumList, common.Config.DefaultForum, common.TopicListSort{torder, false}, paginator}
return renderTemplate("topics", w, r, header, pi) return renderTemplate("topics", w, r, header, pi)
} }

View File

@ -10,6 +10,9 @@
</div> </div>
</div> </div>
</form> </form>
<div id="panel_analytics_forums_chart" class="colstack_graph_holder">
<div class="ct_chart"></div>
</div>
<div id="panel_analytics_routes" class="colstack_item rowlist"> <div id="panel_analytics_routes" class="colstack_item rowlist">
{{range .ItemList}} {{range .ItemList}}
<div class="rowitem panel_compactrow editable_parent"> <div class="rowitem panel_compactrow editable_parent">
@ -20,4 +23,5 @@
</div> </div>
</main> </main>
</div> </div>
{{template "panel_analytics_script.html" . }}
{{template "footer.html" . }} {{template "footer.html" . }}

View File

@ -10,6 +10,9 @@
</div> </div>
</div> </div>
</form> </form>
<div id="panel_analytics_langs_chart" class="colstack_graph_holder">
<div class="ct_chart"></div>
</div>
<div id="panel_analytics_langs" class="colstack_item rowlist"> <div id="panel_analytics_langs" class="colstack_item rowlist">
{{range .ItemList}} {{range .ItemList}}
<div class="rowitem panel_compactrow editable_parent"> <div class="rowitem panel_compactrow editable_parent">
@ -20,4 +23,5 @@
</div> </div>
</main> </main>
</div> </div>
{{template "panel_analytics_script.html" . }}
{{template "footer.html" . }} {{template "footer.html" . }}

View File

@ -19,7 +19,7 @@
<div id="panel_analytics_posts_table" class="colstack_item rowlist" aria-label="{{lang "panel_statistics_post_counts_table_aria"}}"> <div id="panel_analytics_posts_table" class="colstack_item rowlist" aria-label="{{lang "panel_statistics_post_counts_table_aria"}}">
{{range .ViewItems}} {{range .ViewItems}}
<div class="rowitem panel_compactrow editable_parent"> <div class="rowitem panel_compactrow editable_parent">
<a class="panel_upshift unix_to_24_hour_time">{{.Time}}</a> <a class="panel_upshift {{if or (or (or (eq $.TimeRange "six-hours") (eq $.TimeRange "twelve-hours")) (eq $.TimeRange "one-day")) (eq $.TimeRange "two-days")}}unix_to_24_hour_time{{else}}unix_to_date{{end}}">{{.Time}}</a>
<span class="panel_compacttext to_right">{{.Count}}{{lang "panel_statistics_posts_suffix"}}</span> <span class="panel_compacttext to_right">{{.Count}}{{lang "panel_statistics_posts_suffix"}}</span>
</div> </div>
{{else}}<div class="rowitem passive rowmsg">{{lang "panel_statistics_post_counts_no_post_counts"}}</div>{{end}} {{else}}<div class="rowitem passive rowmsg">{{lang "panel_statistics_post_counts_no_post_counts"}}</div>{{end}}

View File

@ -10,6 +10,9 @@
</div> </div>
</div> </div>
</form> </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"> <div id="panel_analytics_routes" class="colstack_item rowlist">
{{range .ItemList}} {{range .ItemList}}
<div class="rowitem panel_compactrow editable_parent"> <div class="rowitem panel_compactrow editable_parent">
@ -20,4 +23,5 @@
</div> </div>
</main> </main>
</div> </div>
{{template "panel_analytics_script.html" . }}
{{template "footer.html" . }} {{template "footer.html" . }}

View File

@ -10,5 +10,5 @@ let seriesData = [{{range .Graph.Series}}[{{range .}}
let legendNames = [{{range .Graph.Legends}} let legendNames = [{{range .Graph.Legends}}
{{.}},{{end}} {{.}},{{end}}
]; ];
buildStatsChart(rawLabels, seriesData.reverse(), "{{.TimeRange}}",legendNames.reverse()); buildStatsChart(rawLabels, seriesData, "{{.TimeRange}}",legendNames);
</script> </script>

View File

@ -10,6 +10,9 @@
</div> </div>
</div> </div>
</form> </form>
<div id="panel_analytics_systems_chart" class="colstack_graph_holder">
<div class="ct_chart"></div>
</div>
<div id="panel_analytics_systems" class="colstack_item rowlist"> <div id="panel_analytics_systems" class="colstack_item rowlist">
{{range .ItemList}} {{range .ItemList}}
<div class="rowitem panel_compactrow editable_parent"> <div class="rowitem panel_compactrow editable_parent">
@ -20,4 +23,5 @@
</div> </div>
</main> </main>
</div> </div>
{{template "panel_analytics_script.html" . }}
{{template "footer.html" . }} {{template "footer.html" . }}

View File

@ -1,4 +1,5 @@
<select class="timeRangeSelector to_right" name="timeRange"> <select class="timeRangeSelector to_right" name="timeRange">
<option val="one-year"{{if eq .TimeRange "one-year"}} selected{{end}}>{{lang "panel_statistics_time_range_one_year"}}</option>
<option val="three-months"{{if eq .TimeRange "three-months"}} selected{{end}}>{{lang "panel_statistics_time_range_three_months"}}</option> <option val="three-months"{{if eq .TimeRange "three-months"}} selected{{end}}>{{lang "panel_statistics_time_range_three_months"}}</option>
<option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option> <option val="one-month"{{if eq .TimeRange "one-month"}} selected{{end}}>{{lang "panel_statistics_time_range_one_month"}}</option>
<option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option> <option val="one-week"{{if eq .TimeRange "one-week"}} selected{{end}}>{{lang "panel_statistics_time_range_one_week"}}</option>

View File

@ -19,7 +19,7 @@
<div id="panel_analytics_topics_table" class="colstack_item rowlist" aria-label="{{lang "panel_statistics_topic_counts_table_aria"}}"> <div id="panel_analytics_topics_table" class="colstack_item rowlist" aria-label="{{lang "panel_statistics_topic_counts_table_aria"}}">
{{range .ViewItems}} {{range .ViewItems}}
<div class="rowitem panel_compactrow editable_parent"> <div class="rowitem panel_compactrow editable_parent">
<a class="panel_upshift unix_to_24_hour_time">{{.Time}}</a> <a class="panel_upshift {{if or (or (or (eq $.TimeRange "six-hours") (eq $.TimeRange "twelve-hours")) (eq $.TimeRange "one-day")) (eq $.TimeRange "two-days")}}unix_to_24_hour_time{{else}}unix_to_date{{end}}">{{.Time}}</a>
<span class="panel_compacttext to_right">{{.Count}}{{lang "panel_statistics_topics_suffix"}}</span> <span class="panel_compacttext to_right">{{.Count}}{{lang "panel_statistics_topics_suffix"}}</span>
</div> </div>
{{end}} {{end}}

View File

@ -19,7 +19,7 @@
<div id="panel_analytics_views_table" class="colstack_item rowlist" aria-label="{{lang "panel_statistics_requests_table_aria"}}"> <div id="panel_analytics_views_table" class="colstack_item rowlist" aria-label="{{lang "panel_statistics_requests_table_aria"}}">
{{range .ViewItems}} {{range .ViewItems}}
<div class="rowitem panel_compactrow editable_parent"> <div class="rowitem panel_compactrow editable_parent">
<a class="panel_upshift unix_to_24_hour_time">{{.Time}}</a> <a class="panel_upshift {{if or (or (or (eq $.TimeRange "six-hours") (eq $.TimeRange "twelve-hours")) (eq $.TimeRange "one-day")) (eq $.TimeRange "two-days")}}unix_to_24_hour_time{{else}}unix_to_date{{end}}">{{.Time}}</a>
<span class="panel_compacttext to_right">{{.Count}}{{lang "panel_statistics_views_suffix"}}</span> <span class="panel_compacttext to_right">{{.Count}}{{lang "panel_statistics_views_suffix"}}</span>
</div> </div>
{{end}} {{end}}

View File

@ -87,6 +87,10 @@
padding-right: 6px; padding-right: 6px;
} }
#panel_users .rowitem {
padding-left: 4px;
padding-right: 4px;
}
button, .formbutton, .panel_right_button:not(.has_inner_button), #panel_users .profile_url { button, .formbutton, .panel_right_button:not(.has_inner_button), #panel_users .profile_url {
background: rgb(100,100,200); background: rgb(100,100,200);
} }