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:
Azareal 2018-01-03 07:46:18 +00:00
parent 1639d81618
commit dcfcd08248
13 changed files with 270 additions and 36 deletions

View File

@ -149,6 +149,20 @@ type PanelDashboardPage struct {
GridItems []GridElement 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 { type PanelThemesPage struct {
Title string Title string
CurrentUser User CurrentUser User

View File

@ -4,7 +4,6 @@ import (
//"fmt" //"fmt"
"bytes" "bytes"
"html" "html"
"log"
"net/url" "net/url"
"regexp" "regexp"
"strconv" "strconv"
@ -183,6 +182,8 @@ func PreparseMessage(msg string) string {
msg = "" msg = ""
var inBold = false var inBold = false
var inItalic = false var inItalic = false
var inStrike = false
var inUnderline = false
var stepForward = func(i int, step int, runes []rune) int { var stepForward = func(i int, step int, runes []rune) int {
i += step i += step
if i < len(runes) { if i < len(runes) {
@ -192,36 +193,45 @@ func PreparseMessage(msg string) string {
} }
for i := 0; i < len(runes); i++ { for i := 0; i < len(runes); i++ {
char := 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) == ';' { 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) i = stepForward(i, 4, runes)
char := runes[i] char := runes[i]
if char == '/' { if char == '/' {
log.Print("in /")
i = stepForward(i, 1, runes) i = stepForward(i, 1, runes)
char := runes[i] char := runes[i]
if inItalic && char == 'e' && peekMatch(i, "m&gt;", runes) { if inItalic && char == 'e' && peekMatch(i, "m&gt;", runes) {
log.Print("in inItalic")
i += 5 i += 5
inItalic = false inItalic = false
msg += "</em>" msg += "</em>"
} else if inBold && char == 's' && peekMatch(i, "trong&gt;", runes) { } else if inBold && char == 's' && peekMatch(i, "trong&gt;", runes) {
log.Print("in inBold")
i += 9 i += 9
inBold = false inBold = false
msg += "</strong>" msg += "</strong>"
} else if inStrike && char == 'd' && peekMatch(i, "el&gt;", runes) {
i += 6
inStrike = false
msg += "</del>"
} else if inUnderline && char == 'u' && peekMatch(i, "&gt;", runes) {
i += 4
inUnderline = false
msg += "</u>"
} }
} else if !inItalic && char == 'e' && peekMatch(i, "m&gt;", runes) { } else if !inItalic && char == 'e' && peekMatch(i, "m&gt;", runes) {
log.Print("in !inItalic")
i += 5 i += 5
inItalic = true inItalic = true
msg += "<em>" msg += "<em>"
} else if !inBold && char == 's' && peekMatch(i, "trong&gt;", runes) { } else if !inBold && char == 's' && peekMatch(i, "trong&gt;", runes) {
log.Print("in !inBold")
i += 9 i += 9
inBold = true inBold = true
msg += "<strong>" msg += "<strong>"
} else if !inStrike && char == 'd' && peekMatch(i, "el&gt;", runes) {
i += 6
inStrike = true
msg += "<del>"
} else if !inUnderline && char == 'u' && peekMatch(i, "&gt;", runes) {
i += 4
inUnderline = true
msg += "<u>"
} }
} else { } else {
msg += string(char) msg += string(char)
@ -234,6 +244,12 @@ func PreparseMessage(msg string) string {
if inBold { if inBold {
msg += "</strong>" msg += "</strong>"
} }
if inStrike {
msg += "</del>"
}
if inUnderline {
msg += "</u>"
}
return shortcodeToUnicode(msg) return shortcodeToUnicode(msg)
} }

View File

@ -14,8 +14,10 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"time"
"./common" "./common"
"./query_gen/lib"
"github.com/Azareal/gopsutil/mem" "github.com/Azareal/gopsutil/mem"
) )
@ -430,14 +432,72 @@ func routePanelAnalyticsViews(w http.ResponseWriter, r *http.Request, user commo
if ferr != nil { if ferr != nil {
return ferr 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.PreRenderHooks["pre_render_panel_analytics"] != nil {
if common.RunPreRenderHook("pre_render_panel_analytics", w, r, &user, &pi) { if common.RunPreRenderHook("pre_render_panel_analytics", w, r, &user, &pi) {
return nil 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 { if err != nil {
return common.InternalError(err, w, r) return common.InternalError(err, w, r)
} }

View File

@ -46,6 +46,7 @@ type accSelectBuilder struct {
where string where string
orderby string orderby string
limit string limit string
dateCutoff *dateCutoff // We might want to do this in a slightly less hacky way
build *Accumulator build *Accumulator
} }
@ -60,6 +61,11 @@ func (selectItem *accSelectBuilder) Where(where string) *accSelectBuilder {
return selectItem 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 { func (selectItem *accSelectBuilder) Orderby(orderby string) *accSelectBuilder {
selectItem.orderby = orderby selectItem.orderby = orderby
return selectItem return selectItem
@ -71,6 +77,11 @@ func (selectItem *accSelectBuilder) Limit(limit string) *accSelectBuilder {
} }
func (selectItem *accSelectBuilder) Prepare() *sql.Stmt { 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) return selectItem.build.SimpleSelect(selectItem.table, selectItem.columns, selectItem.where, selectItem.orderby, selectItem.limit)
} }

View File

@ -198,7 +198,7 @@ func (build *Accumulator) Update(table string) *accUpdateBuilder {
} }
func (build *Accumulator) Select(table string) *accSelectBuilder { func (build *Accumulator) Select(table string) *accSelectBuilder {
return &accSelectBuilder{table, "", "", "", "", build} return &accSelectBuilder{table, "", "", "", "", nil, build}
} }
func (build *Accumulator) Insert(table string) *accInsertBuilder { func (build *Accumulator) Insert(table string) *accInsertBuilder {

View File

@ -1,7 +1,6 @@
/* WIP Under Construction */ /* WIP Under Construction */
package qgen package qgen
//import "log"
import "database/sql" import "database/sql"
var Builder *builder var Builder *builder

View File

@ -1,12 +1,18 @@
package qgen package qgen
type dateCutoff struct {
Column string
Quantity int
Unit string
}
type prebuilder struct { type prebuilder struct {
adapter Adapter adapter Adapter
} }
func (build *prebuilder) Select(nlist ...string) *selectPrebuilder { func (build *prebuilder) Select(nlist ...string) *selectPrebuilder {
name := optString(nlist, "_builder") name := optString(nlist, "_builder")
return &selectPrebuilder{name, "", "", "", "", "", build.adapter} return &selectPrebuilder{name, "", "", "", "", "", nil, build.adapter}
} }
func (build *prebuilder) Insert(nlist ...string) *insertPrebuilder { func (build *prebuilder) Insert(nlist ...string) *insertPrebuilder {
@ -89,6 +95,7 @@ type selectPrebuilder struct {
where string where string
orderby string orderby string
limit string limit string
dateCutoff *dateCutoff
build Adapter build Adapter
} }
@ -118,10 +125,23 @@ func (selectItem *selectPrebuilder) Limit(limit string) *selectPrebuilder {
return selectItem 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) { func (selectItem *selectPrebuilder) Text() (string, error) {
return selectItem.build.SimpleSelect(selectItem.name, selectItem.table, selectItem.columns, selectItem.where, selectItem.orderby, selectItem.limit) 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() { func (selectItem *selectPrebuilder) Parse() {
selectItem.build.SimpleSelect(selectItem.name, selectItem.table, selectItem.columns, selectItem.where, selectItem.orderby, selectItem.limit) selectItem.build.SimpleSelect(selectItem.name, selectItem.table, selectItem.columns, selectItem.where, selectItem.orderby, selectItem.limit)
} }

View File

@ -435,7 +435,6 @@ func (adapter *MssqlAdapter) SimpleSelect(name string, table string, columns str
for _, column := range colslice { for _, column := range colslice {
querystr += "[" + strings.TrimSpace(column) + "]," querystr += "[" + strings.TrimSpace(column) + "],"
} }
// Remove the trailing comma
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
querystr += " FROM [" + table + "]" querystr += " FROM [" + table + "]"
@ -510,6 +509,11 @@ func (adapter *MssqlAdapter) SimpleSelect(name string, table string, columns str
return querystr, nil 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) { func (adapter *MssqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
if name == "" { if name == "" {
return "", errors.New("You need a name for this statement") return "", errors.New("You need a name for this statement")

View File

@ -304,9 +304,10 @@ func (adapter *MysqlAdapter) Purge(name string, table string) (string, error) {
return "DELETE FROM `" + table + "`", nil return "DELETE FROM `" + table + "`", nil
} }
// TODO: Add support for BETWEEN x.x
func (adapter *MysqlAdapter) buildWhere(where string) (querystr string, err error) { func (adapter *MysqlAdapter) buildWhere(where string) (querystr string, err error) {
if len(where) != 0 { if len(where) == 0 {
return "", nil
}
querystr = " WHERE" querystr = " WHERE"
for _, loc := range processWhere(where) { for _, loc := range processWhere(where) {
for _, token := range loc.Expr { for _, token := range loc.Expr {
@ -323,9 +324,36 @@ func (adapter *MysqlAdapter) buildWhere(where string) (querystr string, err erro
} }
querystr += " AND" querystr += " AND"
} }
querystr = querystr[0 : len(querystr)-4] return querystr[0 : len(querystr)-4], nil
} }
return querystr, 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 {
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
} }
func (adapter *MysqlAdapter) buildOrderby(orderby string) (querystr string) { 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 " var querystr = "SELECT "
// Slice up the user friendly strings into something easier to process // Slice up the user friendly strings into something easier to process
var colslice = strings.Split(strings.TrimSpace(columns), ",") for _, column := range strings.Split(strings.TrimSpace(columns), ",") {
for _, column := range colslice {
querystr += "`" + strings.TrimSpace(column) + "`," querystr += "`" + strings.TrimSpace(column) + "`,"
} }
querystr = querystr[0 : len(querystr)-1] querystr = querystr[0 : len(querystr)-1]
@ -372,6 +399,37 @@ func (adapter *MysqlAdapter) SimpleSelect(name string, table string, columns str
return querystr, nil 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) { func (adapter *MysqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
if name == "" { if name == "" {
return "", errors.New("You need a name for this statement") return "", errors.New("You need a name for this statement")

View File

@ -252,6 +252,20 @@ func (adapter *PgsqlAdapter) SimpleSelect(name string, table string, columns str
return "", nil 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 // 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) { func (adapter *PgsqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) {
if name == "" { if name == "" {

View File

@ -100,11 +100,6 @@ type Adapter interface {
GetName() string GetName() string
CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) 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) 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) SimpleUpdate(name string, table string, set string, where string) (string, error)
SimpleDelete(name string, table string, where string) (string, error) SimpleDelete(name string, table string, where string) (string, error)
Purge(name string, table string) (string, error) Purge(name string, table string) (string, error)
@ -116,6 +111,8 @@ type Adapter interface {
SimpleInsertInnerJoin(string, DBInsert, DBJoin) (string, error) SimpleInsertInnerJoin(string, DBInsert, DBJoin) (string, error)
SimpleCount(string, string, string, string) (string, error) SimpleCount(string, string, string, string) (string, error)
ComplexSelect(*selectPrebuilder) (string, error)
Builder() *prebuilder Builder() *prebuilder
Write() error Write() error
} }

View File

@ -6,7 +6,34 @@
<div class="rowitem"><a>Views</a></div> <div class="rowitem"><a>Views</a></div>
</div> </div>
<div id="panel_analytics" class="colstack_graph_holder"> <div id="panel_analytics" class="colstack_graph_holder">
<div class="ct-chart"></div>
</div> </div>
</main> </main>
</div> </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" . }} {{template "footer.html" . }}

View File

@ -155,3 +155,17 @@
.perm_preset_default:before { .perm_preset_default:before {
content: "Default"; 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;
}