1
0
mirror of https://github.com/ebosas/microservices.git synced 2025-06-24 22:26:56 +02:00

Build messages page, cache API

This commit is contained in:
ebosas
2021-10-07 16:23:34 +03:00
parent ed3ce70b37
commit 866aff9f75
9 changed files with 213 additions and 41 deletions

27
cmd/server/api.go Normal file
View File

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

View File

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

View File

@ -0,0 +1,21 @@
{{ define "content" }}
<div class="container position-relative h-75">
<div class="position-absolute bottom-50 start-0 w-100 translate-middle-y">
<div class="row">
<div class="col col-md-8 col-lg-6 mx-auto">
<form>
<div class="input-group">
<input type="text" class="form-control" placeholder="Enter message" aria-label="Enter message" aria-describedby="button-send" />
<button class="btn btn-success" type="submit" id="button-send">Send</button>
</div>
</form>
</div>
</div>
</div>
<div id="notifications" class="position-absolute top-0 start-0 w-100">
<div class="row">
<div class="col col-md-8 col-lg-6 mx-auto"></div>
</div>
</div>
</div>
{{ end }}

View File

@ -0,0 +1,27 @@
{{ define "content" }}
<div class="container">
<h3 class="my-4 ps-2">Recent messages ({{ .Count }}/{{ .Total }})</h3>
<table class="table">
<thead>
<tr>
<th scope="col">Time</th>
<th scope="col">Message</th>
<th scope="col">Source</th>
</tr>
</thead>
<tbody>
{{ range .Messages }}
<tr>
<td>{{ .Time | fdate }}</td>
<td>{{ .Text }}</td>
<td>{{ .Source }}</td>
</tr>
{{ else }}
<tr>
<td colspan="3">No messages</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ end }}

View File

@ -0,0 +1,14 @@
{{ define "navbar" }}
<nav class="navbar navbar-expand navbar-light bg-transparent">
<div class="container-fluid">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/messages">Messages</a>
</li>
</ul>
</div>
</nav>
{{ end }}

View File

@ -1,4 +1,4 @@
<!doctype html>
{{ define "layout" }}<!doctype html>
<html lang="en">
<head>
<title>Microservices</title>
@ -8,39 +8,10 @@
</head>
<body>
<div id="root" class="vh-100">
<nav class="navbar navbar-expand navbar-light bg-transparent">
<div class="container-fluid">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/messages">Messages</a>
</li>
</ul>
</div>
</nav>
<div class="container position-relative h-75">
<div class="position-absolute bottom-50 start-0 w-100 translate-middle-y">
<div class="row">
<div class="col col-md-8 col-lg-6 mx-auto">
<form>
<div class="input-group">
<input type="text" class="form-control" placeholder="Enter message" aria-label="Enter message" aria-describedby="button-send" />
<button class="btn btn-success" type="submit" id="button-send">Send</button>
</div>
</form>
</div>
</div>
</div>
<div id="notifications" class="position-absolute top-0 start-0 w-100">
<div class="row">
<div class="col col-md-8 col-lg-6 mx-auto"></div>
</div>
</div>
</div>
{{ template "navbar" . }}
{{ template "content" . }}
</div>
<script src="http://127.0.0.1:8000/index.js"></script>
<!-- <script src="static/build/index.js"></script> -->
</body>
</html>
</html>{{ end }}

52
internal/cache/cache.go vendored Normal file
View File

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

View File

@ -71,13 +71,14 @@ function Home() {
<div className="row">
<div className="col col-md-8 col-lg-6 mx-auto">
<Form sendMessage={sendMessage} />
</div>
</div>
</div>
</div>
<div id="notifications" className="position-absolute top-0 start-0 w-100">
<div className="row">
<div className="col col-md-8 col-lg-6 mx-auto">
{alerts.map((alert) => (
// TODO: duplicate keys possible
<Alert key={alert.time} alert={alert} />
))}
</div>

View File

@ -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 (
<div className="container">
<h3 className="my-4 ps-2">Recent messages (3/67)</h3>
<table className="table">