mirror of https://github.com/status-im/fathom.git
add widget component for totals per period
This commit is contained in:
parent
3c77eb14b1
commit
512a256ebd
|
@ -3,7 +3,7 @@ Ana. Open Source Web Analytics.
|
|||
|
||||
This is nowhere near being usable, let alone stable. Treat as a proof of concept.
|
||||
|
||||
![Screenshot of the Ana dashboard](https://github.com/dannyvankooten/ana/raw/master/assets/img/screenshot.png?2)
|
||||
![Screenshot of the Ana dashboard](https://github.com/dannyvankooten/ana/raw/master/assets/img/screenshot.png?v=3)
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
19
ROADMAP.md
19
ROADMAP.md
|
@ -1,21 +1,21 @@
|
|||
Ana Roadmap
|
||||
===========
|
||||
|
||||
This is a general draft document for thoughts and todo's. This has no structure.
|
||||
This is a general draft document for thoughts and todo's, without any structure to it.
|
||||
|
||||
### What's cooking?
|
||||
|
||||
- Add license file.
|
||||
- Get DB creds from env.
|
||||
- Compare period totals to previous period.
|
||||
- Allow setting custom limit in table overviews.
|
||||
- Allow sorting in table overviews.
|
||||
- Choose a OS license & settle on name.
|
||||
- JS client for consuming API endpoints.
|
||||
- Envelope API responses.
|
||||
- Error handling.
|
||||
- Handle canonical URL's.
|
||||
- Track canonical URL's.
|
||||
- Show referrals.
|
||||
- Country flags.
|
||||
- Geolocate unknown IP addresses periodically.
|
||||
- Mask last 2 bits of IP address.
|
||||
- Geo map?
|
||||
- Mask last part of IP address.
|
||||
|
||||
### Key metrics
|
||||
|
||||
|
@ -28,8 +28,3 @@ This is a general draft document for thoughts and todo's. This has no structure.
|
|||
- Acquisition
|
||||
- Referral's
|
||||
- Search keywords
|
||||
|
||||
### Admin themes
|
||||
|
||||
- Pages, http://pages.revox.io/dashboard/latest/html/index.html#usa
|
||||
- Metronic, http://keenthemes.com/preview/metronic/
|
||||
|
|
6
ana.go
6
ana.go
|
@ -13,7 +13,7 @@ import (
|
|||
|
||||
func main() {
|
||||
|
||||
// test .env file
|
||||
// load .env file
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatal("Error loading .env file")
|
||||
|
@ -28,9 +28,11 @@ func main() {
|
|||
r.HandleFunc("/collect", api.CollectHandler).Methods("GET")
|
||||
r.Handle("/api/session", api.Login).Methods("POST")
|
||||
r.Handle("/api/session", api.Logout).Methods("DELETE")
|
||||
r.Handle("/api/visits/count", api.Authorize(api.GetVisitsCountHandler)).Methods("GET")
|
||||
r.Handle("/api/visits/count/day", api.Authorize(api.GetVisitsDayCountHandler)).Methods("GET")
|
||||
r.Handle("/api/visits/count/realtime", api.Authorize(api.GetVisitsRealtimeCount)).Methods("GET")
|
||||
r.Handle("/api/visits/count/realtime", api.Authorize(api.GetVisitsRealtimeCountHandler)).Methods("GET")
|
||||
r.Handle("/api/visits", api.Authorize(api.GetVisitsHandler)).Methods("GET")
|
||||
r.Handle("/api/pageviews/count", api.Authorize(api.GetPageviewsCountHandler)).Methods("GET")
|
||||
r.Handle("/api/pageviews/count/day", api.Authorize(api.GetPageviewsDayCountHandler)).Methods("GET")
|
||||
r.Handle("/api/pageviews", api.Authorize(api.GetPageviewsHandler)).Methods("GET")
|
||||
r.Handle("/api/languages", api.Authorize(api.GetLanguagesHandler)).Methods("GET")
|
||||
|
|
|
@ -47,6 +47,15 @@ func fillDatapoints(days int, points []Datapoint) []Datapoint {
|
|||
return newPoints
|
||||
}
|
||||
|
||||
func getRequestedLimit(r *http.Request) int {
|
||||
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if err != nil || limit == 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
return limit
|
||||
}
|
||||
|
||||
func getRequestedPeriod(r *http.Request) int {
|
||||
period, err := strconv.Atoi(r.URL.Query().Get("period"))
|
||||
if err != nil || period == 0 {
|
||||
|
|
|
@ -16,7 +16,6 @@ var Login = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|||
checkError(err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("true"))
|
||||
})
|
||||
|
||||
|
@ -29,7 +28,6 @@ var Logout = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("true"))
|
||||
})
|
||||
|
||||
|
|
|
@ -43,6 +43,21 @@ var GetPageviewsHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.R
|
|||
})
|
||||
|
||||
|
||||
// URL: /api/pageviews/count
|
||||
var GetPageviewsCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
period := getRequestedPeriod(r)
|
||||
stmt, err := core.DB.Prepare(`SELECT COUNT(*) FROM visits WHERE timestamp >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL ? day) AND timestamp <= CURRENT_TIMESTAMP`)
|
||||
|
||||
checkError(err)
|
||||
defer stmt.Close()
|
||||
|
||||
var result int
|
||||
stmt.QueryRow(period).Scan(&result)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
|
||||
// URL: /api/pageviews/count/day
|
||||
var GetPageviewsDayCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
period := getRequestedPeriod(r)
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"github.com/dannyvankooten/ana/models"
|
||||
"github.com/dannyvankooten/ana/core"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// URL: /api/visits
|
||||
|
@ -26,11 +25,7 @@ var GetVisitsHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Requ
|
|||
checkError(err)
|
||||
defer stmt.Close()
|
||||
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if limit == 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
limit := getRequestedLimit(r)
|
||||
rows, err := stmt.Query(&limit)
|
||||
checkError(err)
|
||||
|
||||
|
@ -50,11 +45,25 @@ var GetVisitsHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Requ
|
|||
json.NewEncoder(w).Encode(results)
|
||||
})
|
||||
|
||||
// URL: /api/visits/count/realtime
|
||||
var GetVisitsRealtimeCount = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
row := core.DB.QueryRow(`SELECT COUNT(DISTINCT(ip_address)) FROM visits WHERE timestamp >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 3 HOUR_MINUTE) AND timestamp <= CURRENT_TIMESTAMP`)
|
||||
// URL: /api/visits/count
|
||||
var GetVisitsCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
period := getRequestedPeriod(r)
|
||||
stmt, err := core.DB.Prepare(`SELECT COUNT(DISTINCT(ip_address)) FROM visits WHERE timestamp >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL ? day) AND timestamp <= CURRENT_TIMESTAMP`)
|
||||
|
||||
checkError(err)
|
||||
defer stmt.Close()
|
||||
|
||||
var result int
|
||||
row.Scan(&result)
|
||||
stmt.QueryRow(period).Scan(&result)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
|
||||
// URL: /api/visits/count/realtime
|
||||
var GetVisitsRealtimeCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var result int
|
||||
core.DB.QueryRow(`SELECT COUNT(DISTINCT(ip_address)) FROM visits WHERE timestamp >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 3 HOUR_MINUTE) AND timestamp <= CURRENT_TIMESTAMP`).Scan(&result)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(result)
|
||||
|
@ -70,11 +79,7 @@ var GetVisitsDayCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *h
|
|||
checkError(err)
|
||||
defer stmt.Close()
|
||||
|
||||
period, err := strconv.Atoi(r.URL.Query().Get("period"))
|
||||
if err != nil || period == 0 {
|
||||
period = 1
|
||||
}
|
||||
|
||||
period := getRequestedPeriod(r)
|
||||
rows, err := stmt.Query(period)
|
||||
checkError(err)
|
||||
|
||||
|
@ -89,9 +94,6 @@ var GetVisitsDayCountHandler = http.HandlerFunc(func(w http.ResponseWriter, r *h
|
|||
|
||||
results = fillDatapoints(period, results)
|
||||
|
||||
err = rows.Err();
|
||||
checkError(err)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(results)
|
||||
})
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 144 KiB |
|
@ -0,0 +1,46 @@
|
|||
'use strict';
|
||||
|
||||
import { h, render, Component } from 'preact';
|
||||
|
||||
class CountWidget extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
count: 0
|
||||
}
|
||||
|
||||
this.fetchData = this.fetchData.bind(this);
|
||||
this.fetchData(props.period);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if(this.props.period != newProps.period) {
|
||||
this.fetchData(newProps.period)
|
||||
}
|
||||
}
|
||||
|
||||
fetchData(period) {
|
||||
return fetch('/api/' + this.props.endpoint + '/count?period=' + period, {
|
||||
credentials: 'include'
|
||||
}).then((r) => {
|
||||
if( r.ok ) { return r.json(); }
|
||||
throw new Error();
|
||||
}).then((data) => {
|
||||
this.setState({ count: data })
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
||||
<div class="block center-text">
|
||||
<h4 class="no-margin">{this.props.title}</h4>
|
||||
<div class="big">{this.state.count}</div>
|
||||
<div class="muted">last {this.props.period} days</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CountWidget
|
|
@ -12,7 +12,7 @@ class Realtime extends Component {
|
|||
}
|
||||
this.fetchData = this.fetchData.bind(this);
|
||||
this.fetchData();
|
||||
window.setInterval(this.fetchData, 6000);
|
||||
window.setInterval(this.fetchData, 15000);
|
||||
}
|
||||
|
||||
fetchData() {
|
||||
|
|
|
@ -7,6 +7,7 @@ import Graph from '../components/Graph.js';
|
|||
import DatePicker from '../components/DatePicker.js';
|
||||
import Table from '../components/Table.js';
|
||||
import HeaderBar from '../components/HeaderBar.js';
|
||||
import CountWidget from '../components/CountWidget.js';
|
||||
|
||||
class Dashboard extends Component {
|
||||
constructor(props) {
|
||||
|
@ -26,6 +27,14 @@ class Dashboard extends Component {
|
|||
<div class="clear">
|
||||
<DatePicker period={this.state.period} onChoose={(p) => { this.setState({ period: p })}} />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<CountWidget title="Visitors" endpoint="visits" period={this.state.period} />
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<CountWidget title="Pageviews" endpoint="pageviews" period={this.state.period} />
|
||||
</div>
|
||||
</div>
|
||||
<Graph period={this.state.period} />
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
|
|
|
@ -26,10 +26,22 @@
|
|||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.big {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.center-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ body {
|
|||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
h1, h2, h3, h4, h5 {
|
||||
color: #111;
|
||||
margin: 0;
|
||||
|
||||
|
@ -84,3 +84,20 @@ a {
|
|||
font-weight: bold;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h3{
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue