diff --git a/common/pages.go b/common/pages.go index e730fa2a..ca49e263 100644 --- a/common/pages.go +++ b/common/pages.go @@ -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 diff --git a/common/parser.go b/common/parser.go index 0bd6bdb4..0453a810 100644 --- a/common/parser.go +++ b/common/parser.go @@ -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 += "" } else if inBold && char == 's' && peekMatch(i, "trong>", runes) { - log.Print("in inBold") i += 9 inBold = false msg += "" + } else if inStrike && char == 'd' && peekMatch(i, "el>", runes) { + i += 6 + inStrike = false + msg += "" + } else if inUnderline && char == 'u' && peekMatch(i, ">", runes) { + i += 4 + inUnderline = false + msg += "" } } else if !inItalic && char == 'e' && peekMatch(i, "m>", runes) { - log.Print("in !inItalic") i += 5 inItalic = true msg += "" } else if !inBold && char == 's' && peekMatch(i, "trong>", runes) { - log.Print("in !inBold") i += 9 inBold = true msg += "" + } else if !inStrike && char == 'd' && peekMatch(i, "el>", runes) { + i += 6 + inStrike = true + msg += "" + } else if !inUnderline && char == 'u' && peekMatch(i, ">", runes) { + i += 4 + inUnderline = true + msg += "" } } else { msg += string(char) @@ -234,6 +244,12 @@ func PreparseMessage(msg string) string { if inBold { msg += "" } + if inStrike { + msg += "" + } + if inUnderline { + msg += "" + } return shortcodeToUnicode(msg) } diff --git a/panel_routes.go b/panel_routes.go index 28acdc0c..c1a11407 100644 --- a/panel_routes.go +++ b/panel_routes.go @@ -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) } diff --git a/query_gen/lib/acc_builders.go b/query_gen/lib/acc_builders.go index e4136f05..b5e1a367 100644 --- a/query_gen/lib/acc_builders.go +++ b/query_gen/lib/acc_builders.go @@ -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) } diff --git a/query_gen/lib/accumulator.go b/query_gen/lib/accumulator.go index 638b5b20..2c37858f 100644 --- a/query_gen/lib/accumulator.go +++ b/query_gen/lib/accumulator.go @@ -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 { diff --git a/query_gen/lib/builder.go b/query_gen/lib/builder.go index f39f2359..19032de7 100644 --- a/query_gen/lib/builder.go +++ b/query_gen/lib/builder.go @@ -1,7 +1,6 @@ /* WIP Under Construction */ package qgen -//import "log" import "database/sql" var Builder *builder diff --git a/query_gen/lib/micro_builders.go b/query_gen/lib/micro_builders.go index 35e64123..530467ca 100644 --- a/query_gen/lib/micro_builders.go +++ b/query_gen/lib/micro_builders.go @@ -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) } diff --git a/query_gen/lib/mssql.go b/query_gen/lib/mssql.go index b758c939..9c151295 100644 --- a/query_gen/lib/mssql.go +++ b/query_gen/lib/mssql.go @@ -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") diff --git a/query_gen/lib/mysql.go b/query_gen/lib/mysql.go index 466bed9f..257269e6 100644 --- a/query_gen/lib/mysql.go +++ b/query_gen/lib/mysql.go @@ -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") diff --git a/query_gen/lib/pgsql.go b/query_gen/lib/pgsql.go index ae11560e..8403e3e9 100644 --- a/query_gen/lib/pgsql.go +++ b/query_gen/lib/pgsql.go @@ -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 == "" { diff --git a/query_gen/lib/querygen.go b/query_gen/lib/querygen.go index 0e985a2c..96853ddf 100644 --- a/query_gen/lib/querygen.go +++ b/query_gen/lib/querygen.go @@ -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 } diff --git a/templates/panel-analytics-views.html b/templates/panel-analytics-views.html index 03546766..0d12c9cc 100644 --- a/templates/panel-analytics-views.html +++ b/templates/panel-analytics-views.html @@ -6,7 +6,34 @@
Views
+
+ {{template "footer.html" . }} diff --git a/themes/cosora/public/panel.css b/themes/cosora/public/panel.css index f9d75d73..414062c6 100644 --- a/themes/cosora/public/panel.css +++ b/themes/cosora/public/panel.css @@ -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; } \ No newline at end of file