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 {
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])
}
oi := 0
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)
topic := ESTopic{}
err := rows.Scan(&topic.ID, &topic.Title, &topic.Content, &topic.CreatedBy, &topic.IPAddress)
if err != nil {
return err
}
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
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
}
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
rin[oi] <- reply
if oi < 3 {
oi++
}
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
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
}
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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",

View File

@ -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";

View File

@ -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();

View File

@ -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

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 {
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)

View File

@ -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)
}

View File

@ -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)
}

View File

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

View File

@ -10,6 +10,9 @@
</div>
</div>
</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">
{{range .ItemList}}
<div class="rowitem panel_compactrow editable_parent">
@ -20,4 +23,5 @@
</div>
</main>
</div>
{{template "panel_analytics_script.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"}}">
{{range .ViewItems}}
<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>
</div>
{{else}}<div class="rowitem passive rowmsg">{{lang "panel_statistics_post_counts_no_post_counts"}}</div>{{end}}

View File

@ -10,6 +10,9 @@
</div>
</div>
</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">
@ -20,4 +23,5 @@
</div>
</main>
</div>
{{template "panel_analytics_script.html" . }}
{{template "footer.html" . }}

View File

@ -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);
</script>

View File

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

View File

@ -1,4 +1,5 @@
<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="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>

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"}}">
{{range .ViewItems}}
<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>
</div>
{{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"}}">
{{range .ViewItems}}
<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>
</div>
{{end}}

View File

@ -87,6 +87,10 @@
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 {
background: rgb(100,100,200);
}