You've already forked microservices
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:
27
cmd/server/api.go
Normal file
27
cmd/server/api.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
21
cmd/server/template/home.html
Normal file
21
cmd/server/template/home.html
Normal 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 }}
|
27
cmd/server/template/messages.html
Normal file
27
cmd/server/template/messages.html
Normal 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 }}
|
14
cmd/server/template/navbar.html
Normal file
14
cmd/server/template/navbar.html
Normal 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 }}
|
@ -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
52
internal/cache/cache.go
vendored
Normal 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
|
||||
}
|
@ -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>
|
||||
|
@ -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">
|
||||
|
Reference in New Issue
Block a user