Added the views graph to the Control Panel.
Added support for strikethrough and underline HTML. Added DateCutoff to the Accumulator Select Builder for MySQL.
This commit is contained in:
parent
1639d81618
commit
dcfcd08248
|
@ -149,6 +149,20 @@ type PanelDashboardPage struct {
|
|||
GridItems []GridElement
|
||||
}
|
||||
|
||||
type PanelTimeGraph struct {
|
||||
Series []int64 // The counts on the left
|
||||
Labels []int64 // unixtimes for the bottom, gets converted into 1:00, 2:00, etc. with JS
|
||||
}
|
||||
|
||||
type PanelAnalyticsPage struct {
|
||||
Title string
|
||||
CurrentUser User
|
||||
Header *HeaderVars
|
||||
Stats PanelStats
|
||||
Zone string
|
||||
PrimaryGraph PanelTimeGraph
|
||||
}
|
||||
|
||||
type PanelThemesPage struct {
|
||||
Title string
|
||||
CurrentUser User
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
//"fmt"
|
||||
"bytes"
|
||||
"html"
|
||||
"log"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
@ -183,6 +182,8 @@ func PreparseMessage(msg string) string {
|
|||
msg = ""
|
||||
var inBold = false
|
||||
var inItalic = false
|
||||
var inStrike = false
|
||||
var inUnderline = false
|
||||
var stepForward = func(i int, step int, runes []rune) int {
|
||||
i += step
|
||||
if i < len(runes) {
|
||||
|
@ -192,36 +193,45 @@ func PreparseMessage(msg string) string {
|
|||
}
|
||||
for i := 0; i < len(runes); i++ {
|
||||
char := runes[i]
|
||||
log.Print("string(char): ", string(char))
|
||||
if char == '&' && peek(i, 1, runes) == 'l' && peek(i, 2, runes) == 't' && peek(i, 3, runes) == ';' {
|
||||
log.Print("past less than")
|
||||
i = stepForward(i, 4, runes)
|
||||
char := runes[i]
|
||||
if char == '/' {
|
||||
log.Print("in /")
|
||||
i = stepForward(i, 1, runes)
|
||||
char := runes[i]
|
||||
if inItalic && char == 'e' && peekMatch(i, "m>", runes) {
|
||||
log.Print("in inItalic")
|
||||
i += 5
|
||||
inItalic = false
|
||||
msg += "</em>"
|
||||
} else if inBold && char == 's' && peekMatch(i, "trong>", runes) {
|
||||
log.Print("in inBold")
|
||||
i += 9
|
||||
inBold = false
|
||||
msg += "</strong>"
|
||||
} else if inStrike && char == 'd' && peekMatch(i, "el>", runes) {
|
||||
i += 6
|
||||
inStrike = false
|
||||
msg += "</del>"
|
||||
} else if inUnderline && char == 'u' && peekMatch(i, ">", runes) {
|
||||
i += 4
|
||||
inUnderline = false
|
||||
msg += "</u>"
|
||||
}
|
||||
} else if !inItalic && char == 'e' && peekMatch(i, "m>", runes) {
|
||||
log.Print("in !inItalic")
|
||||
i += 5
|
||||
inItalic = true
|
||||
msg += "<em>"
|
||||
} else if !inBold && char == 's' && peekMatch(i, "trong>", runes) {
|
||||
log.Print("in !inBold")
|
||||
i += 9
|
||||
inBold = true
|
||||
msg += "<strong>"
|
||||
} else if !inStrike && char == 'd' && peekMatch(i, "el>", runes) {
|
||||
i += 6
|
||||
inStrike = true
|
||||
msg += "<del>"
|
||||
} else if !inUnderline && char == 'u' && peekMatch(i, ">", runes) {
|
||||
i += 4
|
||||
inUnderline = true
|
||||
msg += "<u>"
|
||||
}
|
||||
} else {
|
||||
msg += string(char)
|
||||
|
@ -234,6 +244,12 @@ func PreparseMessage(msg string) string {
|
|||
if inBold {
|
||||
msg += "</strong>"
|
||||
}
|
||||
if inStrike {
|
||||
msg += "</del>"
|
||||
}
|
||||
if inUnderline {
|
||||
msg += "</u>"
|
||||
}
|
||||
|
||||
return shortcodeToUnicode(msg)
|
||||
}
|
||||
|
|
|
@ -14,8 +14,10 @@ import (
|
|||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"./common"
|
||||
"./query_gen/lib"
|
||||
"github.com/Azareal/gopsutil/mem"
|
||||
)
|
||||
|
||||
|
@ -430,14 +432,72 @@ func routePanelAnalyticsViews(w http.ResponseWriter, r *http.Request, user commo
|
|||
if ferr != nil {
|
||||
return ferr
|
||||
}
|
||||
headerVars.Stylesheets = append(headerVars.Stylesheets, "chartist/chartist.min.css")
|
||||
headerVars.Scripts = append(headerVars.Scripts, "chartist/chartist.min.js")
|
||||
|
||||
pi := common.PanelPage{common.GetTitlePhrase("panel-analytics"), user, headerVars, stats, "analytics", tList, nil}
|
||||
var revLabelList []int64
|
||||
var labelList []int64
|
||||
var viewMap = make(map[int64]int64)
|
||||
var currentTime = time.Now().Unix()
|
||||
|
||||
for i := 1; i <= 12; i++ {
|
||||
var label = currentTime - int64(i*60*30)
|
||||
revLabelList = append(revLabelList, label)
|
||||
viewMap[label] = 0
|
||||
}
|
||||
for _, value := range revLabelList {
|
||||
labelList = append(labelList, value)
|
||||
}
|
||||
|
||||
var viewList []int64
|
||||
|
||||
acc := qgen.Builder.Accumulator()
|
||||
rows, err := acc.Select("viewchunks").Columns("count, createdAt, route").Where("route = ''").DateCutoff("createdAt", 6, "hour").Query()
|
||||
if err != nil && err != ErrNoRows {
|
||||
return common.InternalError(err, w, r)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
log.Print("WE HAVE ROWS")
|
||||
var count int64
|
||||
var createdAt time.Time
|
||||
var route string
|
||||
err := rows.Scan(&count, &createdAt, &route)
|
||||
if err != nil {
|
||||
return common.InternalError(err, w, r)
|
||||
}
|
||||
log.Print("count: ", count)
|
||||
log.Print("createdAt: ", createdAt)
|
||||
log.Print("route: ", route)
|
||||
|
||||
var unixCreatedAt = createdAt.Unix()
|
||||
log.Print("unixCreatedAt: ", unixCreatedAt)
|
||||
for _, value := range labelList {
|
||||
if unixCreatedAt > value {
|
||||
viewMap[value] += count
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return common.InternalError(err, w, r)
|
||||
}
|
||||
|
||||
for _, value := range revLabelList {
|
||||
viewList = append(viewList, viewMap[value])
|
||||
}
|
||||
graph := common.PanelTimeGraph{Series: viewList, Labels: labelList}
|
||||
log.Printf("graph: %+v\n", graph)
|
||||
|
||||
pi := common.PanelAnalyticsPage{common.GetTitlePhrase("panel-analytics"), user, headerVars, stats, "analytics", graph}
|
||||
if common.PreRenderHooks["pre_render_panel_analytics"] != nil {
|
||||
if common.RunPreRenderHook("pre_render_panel_analytics", w, r, &user, &pi) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
err := common.Templates.ExecuteTemplate(w, "panel-analytics-views.html", pi)
|
||||
err = common.Templates.ExecuteTemplate(w, "panel-analytics-views.html", pi)
|
||||
if err != nil {
|
||||
return common.InternalError(err, w, r)
|
||||
}
|
||||
|
|
|
@ -41,11 +41,12 @@ func (update *accUpdateBuilder) Prepare() *sql.Stmt {
|
|||
}
|
||||
|
||||
type accSelectBuilder struct {
|
||||
table string
|
||||
columns string
|
||||
where string
|
||||
orderby string
|
||||
limit string
|
||||
table string
|
||||
columns string
|
||||
where string
|
||||
orderby string
|
||||
limit string
|
||||
dateCutoff *dateCutoff // We might want to do this in a slightly less hacky way
|
||||
|
||||
build *Accumulator
|
||||
}
|
||||
|
@ -60,6 +61,11 @@ func (selectItem *accSelectBuilder) Where(where string) *accSelectBuilder {
|
|||
return selectItem
|
||||
}
|
||||
|
||||
func (selectItem *accSelectBuilder) DateCutoff(column string, quantity int, unit string) *accSelectBuilder {
|
||||
selectItem.dateCutoff = &dateCutoff{column, quantity, unit}
|
||||
return selectItem
|
||||
}
|
||||
|
||||
func (selectItem *accSelectBuilder) Orderby(orderby string) *accSelectBuilder {
|
||||
selectItem.orderby = orderby
|
||||
return selectItem
|
||||
|
@ -71,6 +77,11 @@ func (selectItem *accSelectBuilder) Limit(limit string) *accSelectBuilder {
|
|||
}
|
||||
|
||||
func (selectItem *accSelectBuilder) Prepare() *sql.Stmt {
|
||||
// TODO: Phase out the procedural API and use the adapter's OO API? The OO API might need a bit more work before we do that and it needs to be rolled out to MSSQL.
|
||||
if selectItem.dateCutoff != nil {
|
||||
selectBuilder := selectItem.build.GetAdapter().Builder().Select().FromAcc(selectItem)
|
||||
return selectItem.build.prepare(selectItem.build.GetAdapter().ComplexSelect(selectBuilder))
|
||||
}
|
||||
return selectItem.build.SimpleSelect(selectItem.table, selectItem.columns, selectItem.where, selectItem.orderby, selectItem.limit)
|
||||
}
|
||||
|
||||
|
|
|
@ -198,7 +198,7 @@ func (build *Accumulator) Update(table string) *accUpdateBuilder {
|
|||
}
|
||||
|
||||
func (build *Accumulator) Select(table string) *accSelectBuilder {
|
||||
return &accSelectBuilder{table, "", "", "", "", build}
|
||||
return &accSelectBuilder{table, "", "", "", "", nil, build}
|
||||
}
|
||||
|
||||
func (build *Accumulator) Insert(table string) *accInsertBuilder {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/* WIP Under Construction */
|
||||
package qgen
|
||||
|
||||
//import "log"
|
||||
import "database/sql"
|
||||
|
||||
var Builder *builder
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
package qgen
|
||||
|
||||
type dateCutoff struct {
|
||||
Column string
|
||||
Quantity int
|
||||
Unit string
|
||||
}
|
||||
|
||||
type prebuilder struct {
|
||||
adapter Adapter
|
||||
}
|
||||
|
||||
func (build *prebuilder) Select(nlist ...string) *selectPrebuilder {
|
||||
name := optString(nlist, "_builder")
|
||||
return &selectPrebuilder{name, "", "", "", "", "", build.adapter}
|
||||
return &selectPrebuilder{name, "", "", "", "", "", nil, build.adapter}
|
||||
}
|
||||
|
||||
func (build *prebuilder) Insert(nlist ...string) *insertPrebuilder {
|
||||
|
@ -83,12 +89,13 @@ func (update *updatePrebuilder) Parse() {
|
|||
}
|
||||
|
||||
type selectPrebuilder struct {
|
||||
name string
|
||||
table string
|
||||
columns string
|
||||
where string
|
||||
orderby string
|
||||
limit string
|
||||
name string
|
||||
table string
|
||||
columns string
|
||||
where string
|
||||
orderby string
|
||||
limit string
|
||||
dateCutoff *dateCutoff
|
||||
|
||||
build Adapter
|
||||
}
|
||||
|
@ -118,10 +125,23 @@ func (selectItem *selectPrebuilder) Limit(limit string) *selectPrebuilder {
|
|||
return selectItem
|
||||
}
|
||||
|
||||
// TODO: We probably want to avoid the double allocation of two builders somehow
|
||||
func (selectItem *selectPrebuilder) FromAcc(accBuilder *accSelectBuilder) *selectPrebuilder {
|
||||
selectItem.table = accBuilder.table
|
||||
selectItem.columns = accBuilder.columns
|
||||
selectItem.where = accBuilder.where
|
||||
selectItem.dateCutoff = accBuilder.dateCutoff
|
||||
selectItem.orderby = accBuilder.orderby
|
||||
selectItem.limit = accBuilder.limit
|
||||
return selectItem
|
||||
}
|
||||
|
||||
// TODO: Add support for dateCutoff
|
||||
func (selectItem *selectPrebuilder) Text() (string, error) {
|
||||
return selectItem.build.SimpleSelect(selectItem.name, selectItem.table, selectItem.columns, selectItem.where, selectItem.orderby, selectItem.limit)
|
||||
}
|
||||
|
||||
// TODO: Add support for dateCutoff
|
||||
func (selectItem *selectPrebuilder) Parse() {
|
||||
selectItem.build.SimpleSelect(selectItem.name, selectItem.table, selectItem.columns, selectItem.where, selectItem.orderby, selectItem.limit)
|
||||
}
|
||||
|
|
|
@ -435,7 +435,6 @@ func (adapter *MssqlAdapter) SimpleSelect(name string, table string, columns str
|
|||
for _, column := range colslice {
|
||||
querystr += "[" + strings.TrimSpace(column) + "],"
|
||||
}
|
||||
// Remove the trailing comma
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
||||
querystr += " FROM [" + table + "]"
|
||||
|
@ -510,6 +509,11 @@ func (adapter *MssqlAdapter) SimpleSelect(name string, table string, columns str
|
|||
return querystr, nil
|
||||
}
|
||||
|
||||
// TODO: ComplexSelect
|
||||
func (adapter *MssqlAdapter) ComplexSelect(preBuilder *selectPrebuilder) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (adapter *MssqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
|
|
|
@ -304,10 +304,39 @@ func (adapter *MysqlAdapter) Purge(name string, table string) (string, error) {
|
|||
return "DELETE FROM `" + table + "`", nil
|
||||
}
|
||||
|
||||
// TODO: Add support for BETWEEN x.x
|
||||
func (adapter *MysqlAdapter) buildWhere(where string) (querystr string, err error) {
|
||||
if len(where) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
querystr = " WHERE"
|
||||
for _, loc := range processWhere(where) {
|
||||
for _, token := range loc.Expr {
|
||||
switch token.Type {
|
||||
case "function", "operator", "number", "substitute":
|
||||
querystr += " " + token.Contents
|
||||
case "column":
|
||||
querystr += " `" + token.Contents + "`"
|
||||
case "string":
|
||||
querystr += " '" + token.Contents + "'"
|
||||
default:
|
||||
return querystr, errors.New("This token doesn't exist o_o")
|
||||
}
|
||||
}
|
||||
querystr += " AND"
|
||||
}
|
||||
return querystr[0 : len(querystr)-4], nil
|
||||
}
|
||||
|
||||
// The new version of buildWhere() currently only used in ComplexSelect for complex OO builder queries
|
||||
func (adapter *MysqlAdapter) buildFlexiWhere(where string, dateCutoff *dateCutoff) (querystr string, err error) {
|
||||
if len(where) == 0 && dateCutoff == nil {
|
||||
return "", nil
|
||||
}
|
||||
querystr = " WHERE"
|
||||
if dateCutoff != nil {
|
||||
querystr += " " + dateCutoff.Column + " BETWEEN (UTC_TIMESTAMP() - interval " + strconv.Itoa(dateCutoff.Quantity) + " " + dateCutoff.Unit + ") AND UTC_TIMESTAMP() AND"
|
||||
}
|
||||
if len(where) != 0 {
|
||||
querystr = " WHERE"
|
||||
for _, loc := range processWhere(where) {
|
||||
for _, token := range loc.Expr {
|
||||
switch token.Type {
|
||||
|
@ -323,9 +352,8 @@ func (adapter *MysqlAdapter) buildWhere(where string) (querystr string, err erro
|
|||
}
|
||||
querystr += " AND"
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-4]
|
||||
}
|
||||
return querystr, nil
|
||||
return querystr[0 : len(querystr)-4], nil
|
||||
}
|
||||
|
||||
func (adapter *MysqlAdapter) buildOrderby(orderby string) (querystr string) {
|
||||
|
@ -354,8 +382,7 @@ func (adapter *MysqlAdapter) SimpleSelect(name string, table string, columns str
|
|||
var querystr = "SELECT "
|
||||
|
||||
// Slice up the user friendly strings into something easier to process
|
||||
var colslice = strings.Split(strings.TrimSpace(columns), ",")
|
||||
for _, column := range colslice {
|
||||
for _, column := range strings.Split(strings.TrimSpace(columns), ",") {
|
||||
querystr += "`" + strings.TrimSpace(column) + "`,"
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
@ -372,6 +399,37 @@ func (adapter *MysqlAdapter) SimpleSelect(name string, table string, columns str
|
|||
return querystr, nil
|
||||
}
|
||||
|
||||
func (adapter *MysqlAdapter) ComplexSelect(preBuilder *selectPrebuilder) (string, error) {
|
||||
if preBuilder.name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if preBuilder.table == "" {
|
||||
return "", errors.New("You need a name for this table")
|
||||
}
|
||||
if len(preBuilder.columns) == 0 {
|
||||
return "", errors.New("No columns found for ComplexSelect")
|
||||
}
|
||||
|
||||
var querystr = "SELECT "
|
||||
|
||||
// Slice up the user friendly strings into something easier to process
|
||||
for _, column := range strings.Split(strings.TrimSpace(preBuilder.columns), ",") {
|
||||
querystr += "`" + strings.TrimSpace(column) + "`,"
|
||||
}
|
||||
querystr = querystr[0 : len(querystr)-1]
|
||||
|
||||
whereStr, err := adapter.buildFlexiWhere(preBuilder.where, preBuilder.dateCutoff)
|
||||
if err != nil {
|
||||
return querystr, err
|
||||
}
|
||||
|
||||
querystr += " FROM `" + preBuilder.table + "`" + whereStr + adapter.buildOrderby(preBuilder.orderby) + adapter.buildLimit(preBuilder.limit)
|
||||
|
||||
querystr = strings.TrimSpace(querystr)
|
||||
adapter.pushStatement(preBuilder.name, "select", querystr)
|
||||
return querystr, nil
|
||||
}
|
||||
|
||||
func (adapter *MysqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
|
||||
if name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
|
|
|
@ -252,6 +252,20 @@ func (adapter *PgsqlAdapter) SimpleSelect(name string, table string, columns str
|
|||
return "", nil
|
||||
}
|
||||
|
||||
// TODO: Implement this
|
||||
func (adapter *PgsqlAdapter) ComplexSelect(prebuilder *selectPrebuilder) (string, error) {
|
||||
if prebuilder.name == "" {
|
||||
return "", errors.New("You need a name for this statement")
|
||||
}
|
||||
if prebuilder.table == "" {
|
||||
return "", errors.New("You need a name for this table")
|
||||
}
|
||||
if len(prebuilder.columns) == 0 {
|
||||
return "", errors.New("No columns found for ComplexSelect")
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// TODO: Implement this
|
||||
func (adapter *PgsqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
|
||||
if name == "" {
|
||||
|
|
|
@ -100,11 +100,6 @@ type Adapter interface {
|
|||
GetName() string
|
||||
CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error)
|
||||
SimpleInsert(name string, table string, columns string, fields string) (string, error)
|
||||
|
||||
// ! DEPRECATED
|
||||
//SimpleReplace(name string, table string, columns string, fields string) (string, error)
|
||||
// ! NOTE: MySQL doesn't support upserts properly, so I'm removing this from the interface until we find a way to patch it in
|
||||
//SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error)
|
||||
SimpleUpdate(name string, table string, set string, where string) (string, error)
|
||||
SimpleDelete(name string, table string, where string) (string, error)
|
||||
Purge(name string, table string) (string, error)
|
||||
|
@ -116,6 +111,8 @@ type Adapter interface {
|
|||
SimpleInsertInnerJoin(string, DBInsert, DBJoin) (string, error)
|
||||
SimpleCount(string, string, string, string) (string, error)
|
||||
|
||||
ComplexSelect(*selectPrebuilder) (string, error)
|
||||
|
||||
Builder() *prebuilder
|
||||
Write() error
|
||||
}
|
||||
|
|
|
@ -6,7 +6,34 @@
|
|||
<div class="rowitem"><a>Views</a></div>
|
||||
</div>
|
||||
<div id="panel_analytics" class="colstack_graph_holder">
|
||||
<div class="ct-chart"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script>
|
||||
let labels = [];
|
||||
let rawLabels = [{{range .PrimaryGraph.Labels}}
|
||||
{{.}},{{end}}
|
||||
];
|
||||
for(const i in rawLabels) {
|
||||
let date = new Date(rawLabels[i]*1000);
|
||||
console.log("date: ", date);
|
||||
let minutes = "0" + date.getMinutes();
|
||||
let label = date.getHours() + ":" + minutes.substr(-2);
|
||||
console.log("label:", label);
|
||||
labels.push(label);
|
||||
}
|
||||
|
||||
let seriesData = [{{range .PrimaryGraph.Series}}
|
||||
{{.}},{{end}}
|
||||
];
|
||||
seriesData = seriesData.reverse();
|
||||
|
||||
Chartist.Line('.ct-chart', {
|
||||
labels: labels,
|
||||
series: [seriesData],
|
||||
}, {
|
||||
height: '250px',
|
||||
});
|
||||
</script>
|
||||
{{template "footer.html" . }}
|
||||
|
|
|
@ -155,3 +155,17 @@
|
|||
.perm_preset_default:before {
|
||||
content: "Default";
|
||||
}
|
||||
|
||||
.colstack_graph_holder {
|
||||
background-color: var(--element-background-color);
|
||||
border: 1px solid var(--element-border-color);
|
||||
border-bottom: 2px solid var(--element-border-color);
|
||||
margin-left: 16px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
.ct-series-a .ct-bar, .ct-series-a .ct-line, .ct-series-a .ct-point, .ct-series-a .ct-slice-donut {
|
||||
stroke: hsl(359,98%,53%) !important;
|
||||
}
|
||||
.ct-point {
|
||||
stroke: hsl(359,98%,33%) !important;
|
||||
}
|
Loading…
Reference in New Issue