prototype report page
This commit is contained in:
@@ -16,6 +16,12 @@ type (
|
||||
db *pgxpool.Pool
|
||||
}
|
||||
|
||||
// StoreWithContext allows currying all Store methods by context.Context
|
||||
StoreWithContext struct {
|
||||
ctx context.Context
|
||||
db *Store
|
||||
}
|
||||
|
||||
Event struct {
|
||||
Platform string
|
||||
StoreID string
|
||||
@@ -51,6 +57,13 @@ func newPool(ctx context.Context) (*pgxpool.Pool, error) {
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
func (db *Store) WithContext(ctx context.Context) *StoreWithContext {
|
||||
return &StoreWithContext{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Store) Save(ctx context.Context, e *Event) error {
|
||||
_, err := db.db.Exec(
|
||||
ctx,
|
||||
@@ -87,3 +100,55 @@ func (db *Store) Save(ctx context.Context, e *Event) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *StoreWithContext) Save(e *Event) error {
|
||||
return db.db.Save(db.ctx, e)
|
||||
}
|
||||
|
||||
func (db *Store) LoadEventsForStore(
|
||||
ctx context.Context,
|
||||
platform string,
|
||||
storeID string,
|
||||
) ([]Event, error) {
|
||||
rows, err := db.db.Query(
|
||||
ctx,
|
||||
`
|
||||
SELECT
|
||||
platform as Platform,
|
||||
store_id as StoreID,
|
||||
event_timestamp as EventTimestamp,
|
||||
event_id as EventID,
|
||||
raw_payload as Payload
|
||||
FROM
|
||||
raw_store_events
|
||||
WHERE
|
||||
platform = @platform
|
||||
AND store_id = @store_id
|
||||
ORDER BY
|
||||
event_timestamp DESC
|
||||
LIMIT
|
||||
100
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"platform": platform,
|
||||
"store_id": storeID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to perform query: %w", err)
|
||||
}
|
||||
|
||||
evts, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[Event])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan events: %w", err)
|
||||
}
|
||||
|
||||
return evts, nil
|
||||
}
|
||||
|
||||
func (db *StoreWithContext) LoadEventsForStore(
|
||||
platform string,
|
||||
storeID string,
|
||||
) ([]Event, error) {
|
||||
return db.db.LoadEventsForStore(db.ctx, platform, storeID)
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+107
-13
@@ -1,26 +1,120 @@
|
||||
package site
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/angelbeltran/templater"
|
||||
|
||||
"ruben/inventory2/internal/domains/raw_events"
|
||||
)
|
||||
|
||||
func NewSiteHandler() http.Handler {
|
||||
func NewSiteHandler(dir string, db *raw_events.Store) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// non-html routes
|
||||
|
||||
scfs := http.FileServer(http.Dir(dir + "/scripts"))
|
||||
mux.Handle("/scripts/", http.StripPrefix("/scripts", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/javascript")
|
||||
if path.Ext(r.URL.Path) == ".gz" {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
}
|
||||
scfs.ServeHTTP(w, r)
|
||||
})))
|
||||
mux.Handle("/styles/", http.StripPrefix("/styles", http.FileServer(http.Dir(dir+"/styles"))))
|
||||
|
||||
// html page routes
|
||||
|
||||
tmplr := templater.NewTemplater(
|
||||
dir+"/templates",
|
||||
func() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"buildSitePath": func(parts ...any) string {
|
||||
strParts := make([]string, len(parts))
|
||||
for i, p := range parts {
|
||||
strParts[i] = fmt.Sprint(p)
|
||||
}
|
||||
|
||||
// TODO: make "/site" dynamic somehow
|
||||
return path.Join(append([]string{"/site"}, strParts...)...)
|
||||
},
|
||||
|
||||
"prettyPrintJSON": func(j json.RawMessage) string {
|
||||
b, err := json.MarshalIndent(j, " ", "")
|
||||
if err != nil {
|
||||
return string(j)
|
||||
}
|
||||
return string(b)
|
||||
},
|
||||
|
||||
"addInt": func(a, b int) int {
|
||||
return a + b
|
||||
},
|
||||
"subInt": func(a, b int) int {
|
||||
return a - b
|
||||
},
|
||||
"multInt": func(a, b int) int {
|
||||
return a * b
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`
|
||||
<DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>TEST</title>
|
||||
</head>
|
||||
<body>
|
||||
TEST
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
b, err := tmplr.ExecutePage(
|
||||
getPageTemplateNameForURL(r.URL),
|
||||
"Request",
|
||||
r,
|
||||
// add services here
|
||||
"RawEvents",
|
||||
db.WithContext(r.Context()),
|
||||
"URLCalc",
|
||||
newURLCalculator(r.URL),
|
||||
)
|
||||
if err != nil {
|
||||
// TODO: handle 'not found' as a 404?
|
||||
fmt.Println("[ERROR]: failed to load or parse layout template:", err)
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
})
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func getPageTemplateNameForURL(u *url.URL) string {
|
||||
filepath := strings.TrimPrefix(strings.TrimSuffix(u.Path, ".html"), "/")
|
||||
if filepath == "" {
|
||||
// "/" maps to "/home"
|
||||
filepath = "home"
|
||||
}
|
||||
|
||||
return filepath
|
||||
}
|
||||
|
||||
type URLCalculator struct {
|
||||
url *url.URL
|
||||
}
|
||||
|
||||
func newURLCalculator(u *url.URL) URLCalculator {
|
||||
cpy := *u
|
||||
return URLCalculator{
|
||||
url: &cpy,
|
||||
}
|
||||
}
|
||||
|
||||
func (c URLCalculator) SetQueryParam(k string, v any) string {
|
||||
u := *c.url
|
||||
q := u.Query()
|
||||
q.Set(k, fmt.Sprint(v))
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
ul, ol {
|
||||
/* remove default agent spacing */
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
& > li {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ buildSitePath }}">
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="{{ buildSitePath "reports" }}">
|
||||
Reports
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WIP</title>
|
||||
|
||||
<link rel="stylesheet" href="/site/styles/index.css">
|
||||
|
||||
<script src="/site/scripts/htmx.min.js.gz"></script>
|
||||
<script src="/site/scripts/_hyperscript.min.js.gz"></script>
|
||||
|
||||
{{/* This is where the page_head template will be inserted into the page */}}
|
||||
{{- block "head" . }}{{ end }}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{/* This is where the page_body template will be inserted into the page */}}
|
||||
{{- block "body" . }}{{ end }}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
{{ componentBody "nav_bar" }}
|
||||
|
||||
<h1>Home</h1>
|
||||
@@ -0,0 +1,89 @@
|
||||
{{ componentBody "nav_bar" }}
|
||||
|
||||
{{ $storeID := .Request.URL.Query.Get "store-id" }}
|
||||
|
||||
<h1>Reports</h1>
|
||||
|
||||
<main>
|
||||
<select
|
||||
_='
|
||||
on change
|
||||
make a URL from window.location.href called currentURL
|
||||
set currentQuery to searchParams of currentURL
|
||||
set storeID to event.target.value
|
||||
js(currentQuery, storeID) currentQuery.set("store-id", storeID) end
|
||||
set search of currentURL to currentQuery.toString()
|
||||
set window.location to currentURL
|
||||
'
|
||||
>
|
||||
{{ if not $storeID }}
|
||||
<option selected disabled>
|
||||
choose a store
|
||||
</option>
|
||||
{{ end }}
|
||||
<option {{ if eq $storeID "test-store-1" }}selected{{ end }} >
|
||||
test-store-1
|
||||
</option>
|
||||
<option {{ if eq $storeID "test-store-2" }}selected{{ end }} >
|
||||
test-store-2
|
||||
</option>
|
||||
<option {{ if eq $storeID "test-store-3" }}selected{{ end }} >
|
||||
test-store-3
|
||||
</option>
|
||||
</select>
|
||||
|
||||
{{- if $storeID }}
|
||||
<section
|
||||
style="
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto 1fr;
|
||||
column-gap: 1em;
|
||||
row-gap: 0.5em;
|
||||
margin: 1em 0;
|
||||
"
|
||||
>
|
||||
<div style="border-bottom: solid black 1px;">
|
||||
Date
|
||||
</div>
|
||||
<div style="border-bottom: solid black 1px;">
|
||||
Time
|
||||
</div>
|
||||
<div style="border-bottom: solid black 1px;">
|
||||
ID
|
||||
</div>
|
||||
<div style="border-bottom: solid black 1px;">
|
||||
Payload
|
||||
</div>
|
||||
{{ $events := .RawEvents.LoadEventsForStore "etsy" $storeID }}
|
||||
{{ range $e := $events }}
|
||||
<div>
|
||||
{{ printf "%d/%02d/%02d" $e.EventTimestamp.Year $e.EventTimestamp.Month $e.EventTimestamp.Day }}
|
||||
</div>
|
||||
<div>
|
||||
{{ $suffix := "am" }}
|
||||
{{ $ts := $e.EventTimestamp }}
|
||||
{{- $hr := $ts.Hour }}
|
||||
{{- if eq $hr 0 }}
|
||||
{{- $hr = 12 }}
|
||||
{{- else if eq $hr 12 }}
|
||||
{{- $suffix = "pm" }}
|
||||
{{- else if gt $hr 12 }}
|
||||
{{- $hr = subInt $hr 12 }}
|
||||
{{- $suffix = "pm" }}
|
||||
{{- end }}
|
||||
|
||||
{{- $hrColorHex := multInt $hr 10 | subInt (subInt 256 128) | printf "%02x" }}
|
||||
{{- $minColorHex := multInt $ts.Minute 2 | subInt (subInt 256 128) | printf "%02x" }}
|
||||
{{- $secColorHex := multInt $ts.Second 2 | subInt (subInt 256 128) | printf "%02x" }}
|
||||
<span style="color: #{{ $hrColorHex }}{{ $hrColorHex }}{{ $hrColorHex }};">{{ printf "%02d" $hr }}</span>:<span style="color: #{{ $minColorHex }}{{ $minColorHex }}{{ $minColorHex }};">{{ printf "%02d" $ts.Minute }}</span>:<span style="color: #{{ $secColorHex }}{{ $secColorHex }}{{ $secColorHex }};">{{ printf "%02d" $ts.Second }}</span>{{ printf " %s" $suffix }}
|
||||
</div>
|
||||
<div>
|
||||
{{ $e.EventID }}
|
||||
</div>
|
||||
<div>
|
||||
{{ prettyPrintJSON $e.Payload }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</section>
|
||||
{{- end }}
|
||||
</main>
|
||||
@@ -21,9 +21,17 @@ func NewWebhookHandler(db *raw_events.Store) http.Handler {
|
||||
|
||||
ts := time.Now().UTC()
|
||||
|
||||
storeID := "test-store-id"
|
||||
var payloadObject struct {
|
||||
StoreID string
|
||||
}
|
||||
if err := json.Unmarshal(body, &payloadObject); err == nil && payloadObject.StoreID != "" {
|
||||
storeID = payloadObject.StoreID
|
||||
}
|
||||
|
||||
err := db.Save(r.Context(), &raw_events.Event{
|
||||
Platform: "etsy",
|
||||
StoreID: "test-store-1",
|
||||
StoreID: storeID,
|
||||
EventID: fmt.Sprint(ts.Unix()),
|
||||
EventTimestamp: ts,
|
||||
Payload: body,
|
||||
|
||||
Reference in New Issue
Block a user