From 4e555b1e3309881de4969794eff19e5275430069 Mon Sep 17 00:00:00 2001 From: Jonni Liljamo Date: Thu, 21 Nov 2024 12:54:52 +0200 Subject: [PATCH] feat: UI for new/patch/delete worker/target --- README.md | 1 + go.mod | 4 +- go.sum | 4 +- internal/components/components.templ | 66 +++++++++- internal/components/index.templ | 14 +- internal/components/partials.templ | 133 +++++++++++++------ internal/handlers/api.go | 189 +++++++++++++++++++++++++++ internal/handlers/handlers.go | 28 ++++ internal/handlers/partials.go | 22 ---- internal/router/router.go | 10 ++ tailwind.config.js | 21 +++ 11 files changed, 421 insertions(+), 71 deletions(-) create mode 100644 internal/handlers/api.go diff --git a/README.md b/README.md index 1940c93..0c46f90 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,4 @@ ## Assets - https://www.svgrepo.com/svg/481486/sheep - Public Domain +- https://www.svgrepo.com/collection/solar-broken-line-icons - CC Attribution diff --git a/go.mod b/go.mod index 030f779..0069244 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.src.quest/~liljamo/emerwen-web go 1.23.3 require ( - git.src.quest/~liljamo/emerwen-proto v0.0.0-20241118080237-06cafd5cb729 + git.src.quest/~liljamo/emerwen-proto v0.0.0-20241120090258-63eb26154b46 github.com/a-h/templ v0.2.793 github.com/alexedwards/scs/v2 v2.8.0 github.com/coreos/go-oidc/v3 v3.11.0 @@ -12,6 +12,7 @@ require ( golang.org/x/oauth2 v0.24.0 golang.org/x/sync v0.8.0 google.golang.org/grpc v1.68.0 + google.golang.org/protobuf v1.35.2 ) require ( @@ -44,6 +45,5 @@ require ( golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/protobuf v1.35.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 028ae64..99a9b01 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.src.quest/~liljamo/emerwen-proto v0.0.0-20241118080237-06cafd5cb729 h1:Zka776qZfycsq3f6VbDrbou3egXl2mCRFp0JkfH1VyY= -git.src.quest/~liljamo/emerwen-proto v0.0.0-20241118080237-06cafd5cb729/go.mod h1:TZkTqP3/rDTcJDTDU61QvV+4+TsZIvW0lt4hOiYCrr0= +git.src.quest/~liljamo/emerwen-proto v0.0.0-20241120090258-63eb26154b46 h1:+kCaPHVSpiXPVB5GzVP68LrgJ9HvYE7p7tnNUHjpGfM= +git.src.quest/~liljamo/emerwen-proto v0.0.0-20241120090258-63eb26154b46/go.mod h1:TZkTqP3/rDTcJDTDU61QvV+4+TsZIvW0lt4hOiYCrr0= github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY= github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w= github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= diff --git a/internal/components/components.templ b/internal/components/components.templ index 769f9d5..35df6f7 100644 --- a/internal/components/components.templ +++ b/internal/components/components.templ @@ -16,19 +16,71 @@ script toggleKeyVisibility(id string) { } templ eyeToggle(t string) { -
- +
-
} + +templ smallDetails(summary string, content string) { +
+ { summary } +
{ content }
+
+} + +templ trash(path string, id string, confirm string) { +
+ + +
+} + +templ buttonSubmit() { +
+ +
+} + +templ buttonAdd() { +
+ +
+} diff --git a/internal/components/index.templ b/internal/components/index.templ index a51330d..ad59a52 100644 --- a/internal/components/index.templ +++ b/internal/components/index.templ @@ -4,8 +4,20 @@ templ Index() { @Base("emerwen, counting sheep") {
if loggedIn(ctx) { -
+ @newWorker() +
}
} } + +templ newWorker() { +
+ new worker +
+ + + +
+
+} diff --git a/internal/components/partials.templ b/internal/components/partials.templ index a99d379..bb16ebb 100644 --- a/internal/components/partials.templ +++ b/internal/components/partials.templ @@ -2,15 +2,16 @@ package components import "git.src.quest/~liljamo/emerwen-proto/go/proto/shared" import "fmt" +import "strings" func getMethodString(target *shared.Target) string { switch target.Method.(type) { case *shared.Target_Ping: - return "Ping" + return "ping" case *shared.Target_Get: - return "Get" + return "get" default: - return "fuck" + return "invalid" } } @@ -18,45 +19,28 @@ templ PartialWorkers(workers []*shared.Worker) {
for _, worker := range workers {
-
- ID: { fmt.Sprintf("%d", worker.Id) } -
-
-
Auth Token:
- +
+ + + +
+ @trash("/api/worker", worker.Id, "delete worker?") +
+ @smallDetails("id", worker.Id) +
+
auth token:
+ - @eyeToggle(fmt.Sprintf("auth_key_%d", worker.Id)) + @eyeToggle(fmt.Sprintf("auth_key_%s", worker.Id))
- Targets: + targets
+ @newTarget(worker.Id) for _, target := range worker.Targets { -
-
- ID: { fmt.Sprintf("%d", target.Id) } -
-
-
Addr:
- -
-
-
Interval (ms):
- -
-
-
Method:
- switch m := target.Method.(type) { - case *shared.Target_Ping: -
Ping
- case *shared.Target_Get: -
Get
- - default: -
fuck
- } -
-
+ @editTarget(target) }
@@ -64,3 +48,78 @@ templ PartialWorkers(workers []*shared.Worker) { }
} + +script selectNewTargetMethod(worker_id string) { + var value = document.getElementById(worker_id + "_new_target_method_select").value; + var get_options = document.getElementById(worker_id + "_new_target_get_options"); + + if (value === "get") { + get_options.style.display = "block"; + } else { + get_options.style.display = "none"; + } +} + +templ newTarget(worker_id string) { +
+ new target +
+ + + + + + + + +
+ + +
+ + + +
+
+} + +templ editTarget(target *shared.Target) { +
+
+ + @trash("/api/target", target.Id, "delete target?") +
+ @smallDetails("id", target.Id) +
+ + +
+
+ + +
+ switch m := target.Method.(type) { + case *shared.Target_Ping: +
method: ping
+ case *shared.Target_Get: +
method: get
+
+ + +
+ default: +
invalid
+ } +
+ + @buttonSubmit() +
+
+} diff --git a/internal/handlers/api.go b/internal/handlers/api.go new file mode 100644 index 0000000..325c1fc --- /dev/null +++ b/internal/handlers/api.go @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2024 Jonni Liljamo + * + * This file is licensed under GPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +package handlers + +import ( + "net/http" + "strconv" + "strings" + + "git.src.quest/~liljamo/emerwen-proto/go/proto" + "git.src.quest/~liljamo/emerwen-proto/go/proto/shared" + "git.src.quest/~liljamo/emerwen-web/internal/client" + "github.com/gin-gonic/gin" +) + +// APIPostWorker returns a gin handler for the post worker route. +func APIPostWorker(client *client.Client) gin.HandlerFunc { + return func(c *gin.Context) { + response, err := client.Client.NewWorker(client.Ctx, &proto.NewWorkerRequest{Name: c.PostForm("name")}) + if err != nil { + handleClientError(c, err) + return + } + + c.Header("HX-Trigger", "refresh-workers") + c.String(http.StatusOK, response.String()) + } +} + +// APIPatchWorker returns a gin handler for the patch worker route. +func APIPatchWorker(client *client.Client) gin.HandlerFunc { + return func(c *gin.Context) { + request := proto.PatchWorkerRequest{ + WorkerId: c.PostForm("id"), + } + + if newName := c.PostForm("name"); newName != "" { + request.Name = &newName + } + + response, err := client.Client.PatchWorker(client.Ctx, &request) + if err != nil { + handleClientError(c, err) + return + } + + c.Header("HX-Trigger", "refresh-workers") + c.String(http.StatusOK, response.String()) + } +} + +// APIDeleteWorker returns a gin handler for the delete worker route. +func APIDeleteWorker(client *client.Client) gin.HandlerFunc { + return func(c *gin.Context) { + response, err := client.Client.DeleteWorker(client.Ctx, &proto.DeleteWorkerRequest{WorkerId: c.Query("id")}) + if err != nil { + handleClientError(c, err) + return + } + + c.Header("HX-Trigger", "refresh-workers") + c.String(http.StatusOK, response.String()) + } +} + +// APIPostTarget returns a gin handler for the post target route. +func APIPostTarget(client *client.Client) gin.HandlerFunc { + return func(c *gin.Context) { + request := proto.NewTargetRequest{ + WorkerId: c.PostForm("worker_id"), + Name: c.PostForm("name"), + Addr: c.PostForm("addr"), + } + + if interval, err := strconv.Atoi(c.PostForm("interval")); err == nil { + request.Interval = int32(interval) + } else { + c.String(http.StatusBadRequest, "Bad interval.") + return + } + + switch c.PostForm("method") { + case "ping": + request.Method = &proto.NewTargetRequest_Ping{} + case "get": + var okCodes []int32 + + if okCodesStr := c.PostForm("ok_codes"); okCodesStr != "" { + for _, codeStr := range strings.Fields(okCodesStr) { + if len(codeStr) != 3 { + c.String(http.StatusBadRequest, "Bad code.") + return + } + + if code, err := strconv.Atoi(codeStr); err == nil { + okCodes = append(okCodes, int32(code)) + } else { + c.String(http.StatusBadRequest, "Bad code.") + return + } + } + } + request.Method = &proto.NewTargetRequest_Get{Get: &shared.MethodGET{OkCodes: okCodes}} + default: + c.String(http.StatusBadRequest, "Bad method.") + return + } + + response, err := client.Client.NewTarget(client.Ctx, &request) + if err != nil { + handleClientError(c, err) + return + } + + c.Header("HX-Trigger", "refresh-workers") + c.String(http.StatusOK, response.String()) + } +} + +// APIPatchTarget returns a gin handler for the patch target route. +func APIPatchTarget(client *client.Client) gin.HandlerFunc { + return func(c *gin.Context) { + request := proto.PatchTargetRequest{ + TargetId: c.PostForm("id"), + } + + if newName := c.PostForm("name"); newName != "" { + request.Name = &newName + } + + if newAddr := c.PostForm("addr"); newAddr != "" { + request.Addr = &newAddr + } + + if interval, err := strconv.Atoi(c.PostForm("interval")); err == nil { + interval := int32(interval) + request.Interval = &interval + } else { + c.String(http.StatusBadRequest, "Bad interval.") + return + } + + if okCodesStr := c.PostForm("ok_codes"); okCodesStr != "" { + var okCodes []int32 + for _, codeStr := range strings.Fields(okCodesStr) { + if len(codeStr) != 3 { + c.String(http.StatusBadRequest, "Bad code.") + return + } + + if code, err := strconv.Atoi(codeStr); err == nil { + okCodes = append(okCodes, int32(code)) + } else { + c.String(http.StatusBadRequest, "Bad code.") + return + } + } + request.Method = &proto.PatchTargetRequest_Get{Get: &shared.MethodGET{OkCodes: okCodes}} + } + + response, err := client.Client.PatchTarget(client.Ctx, &request) + if err != nil { + handleClientError(c, err) + return + } + + c.Header("HX-Trigger", "refresh-workers") + c.String(http.StatusOK, response.String()) + } +} + +// APIDeleteTarget returns a gin handler for the delete target route. +func APIDeleteTarget(client *client.Client) gin.HandlerFunc { + return func(c *gin.Context) { + response, err := client.Client.DeleteTarget(client.Ctx, &proto.DeleteTargetRequest{TargetId: c.Query("id")}) + if err != nil { + handleClientError(c, err) + return + } + + c.Header("HX-Trigger", "refresh-workers") + c.String(http.StatusOK, response.String()) + } +} diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 83883fd..adfee38 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -7,3 +7,31 @@ // Package handlers provides route handlers. package handlers + +import ( + "fmt" + "log/slog" + "net/http" + + "github.com/gin-gonic/gin" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func handleClientError(c *gin.Context, err error) { + if status, ok := status.FromError(err); ok { + slog.Error("RPC error", slog.String("err", err.Error())) + + if status.Code() == codes.Unavailable { + c.String(http.StatusInternalServerError, "RPC unavailable.") + return + } + + c.String(http.StatusInternalServerError, fmt.Sprintf("RPC error: %s", status.String())) + return + } + + slog.Error("Non-RPC error", slog.String("err", err.Error())) + + c.String(http.StatusInternalServerError, "Non-RPC error, see server logs.") +} diff --git a/internal/handlers/partials.go b/internal/handlers/partials.go index efa7c01..7cbb5d4 100644 --- a/internal/handlers/partials.go +++ b/internal/handlers/partials.go @@ -8,37 +8,15 @@ package handlers import ( - "fmt" - "log/slog" "net/http" "git.src.quest/~liljamo/emerwen-web/internal/client" "git.src.quest/~liljamo/emerwen-web/internal/components" "git.src.quest/~liljamo/emerwen-web/internal/renderer" "github.com/gin-gonic/gin" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" ) -func handleClientError(c *gin.Context, err error) { - if status, ok := status.FromError(err); ok { - slog.Error("RPC error", slog.String("err", err.Error())) - - if status.Code() == codes.Unavailable { - c.String(http.StatusInternalServerError, "RPC unavailable.") - return - } - - c.String(http.StatusInternalServerError, fmt.Sprintf("RPC error: %s", status.String())) - return - } - - slog.Error("Non-RPC error", slog.String("err", err.Error())) - - c.String(http.StatusInternalServerError, "Non-RPC error, see server logs.") -} - // PartialWorkers returns a gin handler for the partial workers route. func PartialWorkers(client *client.Client) gin.HandlerFunc { return func(c *gin.Context) { diff --git a/internal/router/router.go b/internal/router/router.go index ae85776..702e549 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -45,6 +45,16 @@ func SetupRouter(l *slog.Logger, a *auth.Auth, sm *scs.SessionManager, client *c s := r.Group("/", middlewares.RequireSession(sm)) { s.GET("/partials/workers", handlers.PartialWorkers(client)) + + a := r.Group("/api") + { + a.POST("/worker", handlers.APIPostWorker(client)) + a.PATCH("/worker", handlers.APIPatchWorker(client)) + a.DELETE("/worker", handlers.APIDeleteWorker(client)) + a.POST("/target", handlers.APIPostTarget(client)) + a.PATCH("/target", handlers.APIPatchTarget(client)) + a.DELETE("/target", handlers.APIDeleteTarget(client)) + } } return r diff --git a/tailwind.config.js b/tailwind.config.js index ade4a2e..4191db2 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -59,10 +59,31 @@ module.exports = { transform: 'translateX(-10%) translateY(75%)' }, }, + lidjump: { + '0%': { + transform: 'translateY(0)', + }, + '50%': { + transform: 'translateY(-75%)', + }, + '100%': { + transform: 'translateY(0)', + }, + }, + fullrotate: { + '0%': { + transform: 'rotate(0deg)', + }, + '100%': { + transform: 'rotate(360deg)', + }, + }, }, animation: { sheepspin: 'sheepspin 750ms linear', sheepmove: 'sheepmove 750ms linear', + lidjump: 'lidjump 500ms linear', + fullrotate: 'fullrotate 500ms linear', }, }, }, -- 2.44.1