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

View File

@ -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&gt;", runes) {
log.Print("in inItalic")
i += 5
inItalic = false
msg += "</em>"
} else if inBold && char == 's' && peekMatch(i, "trong&gt;", runes) {
log.Print("in inBold")
i += 9
inBold = false
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) {
log.Print("in !inItalic")
i += 5
inItalic = true
msg += "<em>"
} else if !inBold && char == 's' && peekMatch(i, "trong&gt;", runes) {
log.Print("in !inBold")
i += 9
inBold = true
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 {
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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -154,4 +154,18 @@
}
.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;
}