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"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ebosas/microservices/internal/cache"
|
||||||
"github.com/ebosas/microservices/internal/config"
|
"github.com/ebosas/microservices/internal/config"
|
||||||
"github.com/ebosas/microservices/internal/rabbit"
|
"github.com/ebosas/microservices/internal/rabbit"
|
||||||
iwebsocket "github.com/ebosas/microservices/internal/websocket"
|
iwebsocket "github.com/ebosas/microservices/internal/websocket"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/streadway/amqp"
|
"github.com/streadway/amqp"
|
||||||
)
|
)
|
||||||
@ -31,30 +33,55 @@ func main() {
|
|||||||
fmt.Println("[Server]")
|
fmt.Println("[Server]")
|
||||||
|
|
||||||
// Establish a Rabbit connection.
|
// Establish a Rabbit connection.
|
||||||
conn, err := rabbit.GetConn(conf.RabbitURL)
|
connMQ, err := rabbit.GetConn(conf.RabbitURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("rabbit connection: %s", err)
|
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 {
|
if err != nil {
|
||||||
log.Fatalf("declare exchange: %s", err)
|
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.Handle("/static/", http.FileServer(http.FS(filesStatic)))
|
||||||
http.HandleFunc("/", handleHome)
|
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))
|
log.Fatal(http.ListenAndServe(conf.ServerAddr, nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleHome handles the home page.
|
||||||
func handleHome(w http.ResponseWriter, r *http.Request) {
|
func handleHome(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/" {
|
if r.URL.Path != "/" {
|
||||||
handleNotFound(w)
|
handleNotFound(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t, _ := template.ParseFS(filesTempl, "template/template.html")
|
t := template.Must(template.ParseFS(filesTempl, "template/template.html", "template/navbar.html", "template/home.html"))
|
||||||
t.Execute(w, nil)
|
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.
|
// 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, _ := template.ParseFS(filesTempl, "template/404.html")
|
||||||
t.Execute(w, nil)
|
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">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Microservices</title>
|
<title>Microservices</title>
|
||||||
@ -8,39 +8,10 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root" class="vh-100">
|
<div id="root" class="vh-100">
|
||||||
<nav class="navbar navbar-expand navbar-light bg-transparent">
|
{{ template "navbar" . }}
|
||||||
<div class="container-fluid">
|
{{ template "content" . }}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<script src="http://127.0.0.1:8000/index.js"></script>
|
<script src="http://127.0.0.1:8000/index.js"></script>
|
||||||
<!-- <script src="static/build/index.js"></script> -->
|
<!-- <script src="static/build/index.js"></script> -->
|
||||||
</body>
|
</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="row">
|
||||||
<div className="col col-md-8 col-lg-6 mx-auto">
|
<div className="col col-md-8 col-lg-6 mx-auto">
|
||||||
<Form sendMessage={sendMessage} />
|
<Form sendMessage={sendMessage} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="notifications" className="position-absolute top-0 start-0 w-100">
|
<div id="notifications" className="position-absolute top-0 start-0 w-100">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col col-md-8 col-lg-6 mx-auto">
|
<div className="col col-md-8 col-lg-6 mx-auto">
|
||||||
{alerts.map((alert) => (
|
{alerts.map((alert) => (
|
||||||
|
// TODO: duplicate keys possible
|
||||||
<Alert key={alert.time} alert={alert} />
|
<Alert key={alert.time} alert={alert} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,27 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
function Messages() {
|
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">
|
<div className="container">
|
||||||
<h3 className="my-4 ps-2">Recent messages (3/67)</h3>
|
<h3 className="my-4 ps-2">Recent messages (3/67)</h3>
|
||||||
<table className="table">
|
<table className="table">
|
||||||
|
Reference in New Issue
Block a user