From 866aff9f75007dc5e28c86aedb95f65d695159e3 Mon Sep 17 00:00:00 2001 From: ebosas Date: Thu, 7 Oct 2021 16:23:34 +0300 Subject: [PATCH] Build messages page, cache API --- cmd/server/api.go | 27 ++++++++++++++++ cmd/server/server.go | 51 ++++++++++++++++++++++++++---- cmd/server/template/home.html | 21 +++++++++++++ cmd/server/template/messages.html | 27 ++++++++++++++++ cmd/server/template/navbar.html | 14 +++++++++ cmd/server/template/template.html | 37 +++------------------- internal/cache/cache.go | 52 +++++++++++++++++++++++++++++++ web/react/src/home.jsx | 3 +- web/react/src/messages.jsx | 22 ++++++++++++- 9 files changed, 213 insertions(+), 41 deletions(-) create mode 100644 cmd/server/api.go create mode 100644 cmd/server/template/home.html create mode 100644 cmd/server/template/messages.html create mode 100644 cmd/server/template/navbar.html create mode 100644 internal/cache/cache.go diff --git a/cmd/server/api.go b/cmd/server/api.go new file mode 100644 index 0000000..ed61894 --- /dev/null +++ b/cmd/server/api.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "github.com/ebosas/microservices/internal/cache" + "github.com/go-redis/redis/v8" +) + +// handleAPICache handles an API call for cached messages +func handleAPICache(cr *redis.Client) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + data, err := cache.GetCacheJSON(cr) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("500 - Something happened")) + + log.Printf("get cache json: %s", err) + return + } + + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, data) + } +} diff --git a/cmd/server/server.go b/cmd/server/server.go index 5e80bfe..315e7b5 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -9,9 +9,11 @@ import ( "net/http" "time" + "github.com/ebosas/microservices/internal/cache" "github.com/ebosas/microservices/internal/config" "github.com/ebosas/microservices/internal/rabbit" iwebsocket "github.com/ebosas/microservices/internal/websocket" + "github.com/go-redis/redis/v8" "github.com/gorilla/websocket" "github.com/streadway/amqp" ) @@ -31,30 +33,55 @@ func main() { fmt.Println("[Server]") // Establish a Rabbit connection. - conn, err := rabbit.GetConn(conf.RabbitURL) + connMQ, err := rabbit.GetConn(conf.RabbitURL) if err != nil { log.Fatalf("rabbit connection: %s", err) } - defer conn.Close() + defer connMQ.Close() - err = conn.DeclareTopicExchange(conf.Exchange) + err = connMQ.DeclareTopicExchange(conf.Exchange) if err != nil { log.Fatalf("declare exchange: %s", err) } + // Redis connection + connR := redis.NewClient(&redis.Options{ + Addr: conf.RedisURL, + Password: "", // no password set + DB: 0, // use default DB + }) + http.Handle("/static/", http.FileServer(http.FS(filesStatic))) http.HandleFunc("/", handleHome) - http.HandleFunc("/ws", handleWebsocketConn(conn)) + http.HandleFunc("/messages", handleMessages(connR)) + http.HandleFunc("/ws", handleWebsocketConn(connMQ)) + http.HandleFunc("/api/cache", handleAPICache(connR)) // defined in api.go log.Fatal(http.ListenAndServe(conf.ServerAddr, nil)) } +// handleHome handles the home page. func handleHome(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { handleNotFound(w) return } - t, _ := template.ParseFS(filesTempl, "template/template.html") - t.Execute(w, nil) + t := template.Must(template.ParseFS(filesTempl, "template/template.html", "template/navbar.html", "template/home.html")) + t.ExecuteTemplate(w, "layout", nil) +} + +// handleMessages handles the messages page. +func handleMessages(cr *redis.Client) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + data, err := cache.GetCache(cr) + if err != nil { + log.Printf("get cache: %s", err) + return + } + + funcMap := template.FuncMap{"fdate": formatTime} + t := template.Must(template.New("").Funcs(funcMap).ParseFS(filesTempl, "template/template.html", "template/navbar.html", "template/messages.html")) + t.ExecuteTemplate(w, "layout", data) + } } // handleWebsocketConn passes a Rabbit connection to the Websocket handler. @@ -145,3 +172,15 @@ func handleNotFound(w http.ResponseWriter) { t, _ := template.ParseFS(filesTempl, "template/404.html") t.Execute(w, nil) } + +// formatTime returns time formatted for display. +func formatTime(timestamp int64) string { + t := time.Unix(timestamp/1000, 0) + + format := "3:04pm, Jan 2" + if t.Day() == time.Now().Day() { + format = "3:04pm" + } + + return t.Format(format) +} diff --git a/cmd/server/template/home.html b/cmd/server/template/home.html new file mode 100644 index 0000000..68b9f0c --- /dev/null +++ b/cmd/server/template/home.html @@ -0,0 +1,21 @@ +{{ define "content" }} +
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+{{ end }} \ No newline at end of file diff --git a/cmd/server/template/messages.html b/cmd/server/template/messages.html new file mode 100644 index 0000000..61a2668 --- /dev/null +++ b/cmd/server/template/messages.html @@ -0,0 +1,27 @@ +{{ define "content" }} +
+

Recent messages ({{ .Count }}/{{ .Total }})

+ + + + + + + + + + {{ range .Messages }} + + + + + + {{ else }} + + + + {{ end }} + +
TimeMessageSource
{{ .Time | fdate }}{{ .Text }}{{ .Source }}
No messages
+
+{{ end }} \ No newline at end of file diff --git a/cmd/server/template/navbar.html b/cmd/server/template/navbar.html new file mode 100644 index 0000000..21eccfc --- /dev/null +++ b/cmd/server/template/navbar.html @@ -0,0 +1,14 @@ +{{ define "navbar" }} + +{{ end }} \ No newline at end of file diff --git a/cmd/server/template/template.html b/cmd/server/template/template.html index 354f43b..70bc464 100644 --- a/cmd/server/template/template.html +++ b/cmd/server/template/template.html @@ -1,4 +1,4 @@ - +{{ define "layout" }} Microservices @@ -8,39 +8,10 @@
- -
-
-
-
-
-
- - -
-
-
-
-
-
-
-
-
-
-
+ {{ template "navbar" . }} + {{ template "content" . }}
- +{{ end }} \ No newline at end of file diff --git a/internal/cache/cache.go b/internal/cache/cache.go new file mode 100644 index 0000000..5926c79 --- /dev/null +++ b/internal/cache/cache.go @@ -0,0 +1,52 @@ +package cache + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/ebosas/microservices/internal/models" + "github.com/go-redis/redis/v8" +) + +// Cache is a data structure for the cache API template. +type Cache struct { + Count int `json:"count"` + Total int `json:"total"` + Messages []models.Message `json:"messages"` +} + +// GetCache gets cached messages from Redis by +// calling GetCacheJSON and unmarshalling the returned JSON. +func GetCache(c *redis.Client) (*Cache, error) { + cacheJSON, err := GetCacheJSON(c) + if err != nil { + return &Cache{}, fmt.Errorf("get cache json: %v", err) + } + + var cache Cache + err = json.Unmarshal([]byte(cacheJSON), &cache) + if err != nil { + return &Cache{}, fmt.Errorf("unmarshal cache: %v", err) + } + + return &cache, nil +} + +// GetCacheJSON reads cached messages from Redis, returns JSON. +func GetCacheJSON(c *redis.Client) (string, error) { + messages, err := c.LRange(context.Background(), "messages", 0, -1).Result() + if err != nil { + return "", fmt.Errorf("lrange redis: %v", err) + } + + total, err := c.Get(context.Background(), "count").Result() + if err != nil { + return "", fmt.Errorf("get redis: %v", err) + } + + cacheJSON := "{\"count\":" + fmt.Sprint(len(messages)) + ",\"total\":" + total + ",\"messages\":[" + strings.Join(messages, ",") + "]}" + + return cacheJSON, nil +} diff --git a/web/react/src/home.jsx b/web/react/src/home.jsx index cfa3b7e..51323db 100644 --- a/web/react/src/home.jsx +++ b/web/react/src/home.jsx @@ -71,13 +71,14 @@ function Home() {
-
+
{alerts.map((alert) => ( + // TODO: duplicate keys possible ))}
diff --git a/web/react/src/messages.jsx b/web/react/src/messages.jsx index 150e33f..96484b1 100644 --- a/web/react/src/messages.jsx +++ b/web/react/src/messages.jsx @@ -1,7 +1,27 @@ import React from "react"; function Messages() { - return ( + const [error, setError] = React.useState(null); + const [isLoaded, setIsLoaded] = React.useState(false); + const [items, setItems] = React.useState([]); + + React.useEffect(() => { + fetch("/api/cache") + .then(res => res.json()) + .then( + (result) => { + setIsLoaded(true); + setItems(result); + console.log(result); + }, + (error) => { + setIsLoaded(true); + setError(error); + } + ) + }, []) + + return (

Recent messages (3/67)