From 3d7fb901d0a6dc2a035a718c3d048fb7bab5e8a6 Mon Sep 17 00:00:00 2001 From: Tine Date: Fri, 23 Feb 2024 12:18:02 +0100 Subject: [PATCH] feat(settings): improve overall design --- internal/handlers/api.go | 25 ++- internal/handlers/authentication.go | 3 +- internal/handlers/settings.go | 2 + internal/handlers/settingsworkers.go | 37 ++-- internal/jwt/jwt.go | 4 - pkg/server/server.go | 1 - pkg/worker/worker.go | 8 + web/static/css/main.css | 29 +++- web/static/css/tailwind.css | 163 +++++++++++++----- web/templates/components/settings.tmpl | 6 +- web/templates/pages/404.tmpl | 2 +- .../pages/settings_healthchecks.tmpl | 32 ++-- .../pages/settings_healthchecks_create.tmpl | 42 ++--- .../pages/settings_healthchecks_describe.tmpl | 110 ++++++------ web/templates/pages/settings_overview.tmpl | 20 +-- web/templates/pages/settings_workers.tmpl | 128 +++++++------- .../pages/settings_workers_create.tmpl | 27 ++- .../pages/settings_workers_describe.tmpl | 51 ++++++ web/templates/tempaltes.go | 1 + 19 files changed, 428 insertions(+), 263 deletions(-) create mode 100644 web/templates/pages/settings_workers_describe.tmpl diff --git a/internal/handlers/api.go b/internal/handlers/api.go index f2834da..7b69628 100644 --- a/internal/handlers/api.go +++ b/internal/handlers/api.go @@ -2,12 +2,14 @@ package handlers import ( "context" + "errors" "net/http" "code.tjo.space/mentos1386/zdravko/internal/models" "code.tjo.space/mentos1386/zdravko/internal/services" "code.tjo.space/mentos1386/zdravko/pkg/api" "github.com/labstack/echo/v4" + "gorm.io/gorm" ) type ApiV1WorkersConnectGETResponse struct { @@ -17,12 +19,21 @@ type ApiV1WorkersConnectGETResponse struct { } func (h *BaseHandler) ApiV1WorkersConnectGET(c echo.Context) error { + ctx := context.Background() cc := c.(AuthenticatedContext) + worker, err := services.GetWorker(ctx, h.query, cc.Principal.Worker.Slug) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return echo.NewHTTPError(http.StatusUnauthorized, "Token invalid") + } + return err + } + response := ApiV1WorkersConnectGETResponse{ Endpoint: h.config.Temporal.ServerHost, - Group: cc.Principal.Worker.Group, - Slug: cc.Principal.Worker.Slug, + Group: worker.Group, + Slug: worker.Slug, } return c.JSON(http.StatusOK, response) @@ -36,8 +47,16 @@ func (h *BaseHandler) ApiV1HealthchecksHistoryPOST(c echo.Context) error { slug := c.Param("slug") + worker, err := services.GetWorker(ctx, h.query, slug) + if err != nil { + return err + } + if worker == nil { + return echo.NewHTTPError(http.StatusNotFound, "Worker not found") + } + var body api.ApiV1HealthchecksHistoryPOSTBody - err := (&echo.DefaultBinder{}).BindBody(c, &body) + err = (&echo.DefaultBinder{}).BindBody(c, &body) if err != nil { return err } diff --git a/internal/handlers/authentication.go b/internal/handlers/authentication.go index 52a6eeb..5f334ad 100644 --- a/internal/handlers/authentication.go +++ b/internal/handlers/authentication.go @@ -102,8 +102,7 @@ func (h *BaseHandler) AuthenticateRequestWithToken(r *http.Request) (*Authentica user = &AuthenticatedUser{} } else if splitSubject[0] == "worker" { worker = &AuthenticatedWorker{ - Slug: splitSubject[1], - Group: claims.WorkerGroup, + Slug: splitSubject[1], } } diff --git a/internal/handlers/settings.go b/internal/handlers/settings.go index a42e31b..abf7aa3 100644 --- a/internal/handlers/settings.go +++ b/internal/handlers/settings.go @@ -35,6 +35,7 @@ var SettingsPages = []*components.Page{ {Path: "/settings/cronjobs", Title: "Cronjobs", Breadcrumb: "Cronjobs"}, {Path: "/settings/workers", Title: "Workers", Breadcrumb: "Workers"}, {Path: "/settings/workers/create", Title: "Workers Create", Breadcrumb: "Create"}, + {Path: "/settings/notifications", Title: "Notifications", Breadcrumb: "Notifications"}, {Path: "/settings/temporal", Title: "Temporal", Breadcrumb: "Temporal"}, {Path: "/oauth2/logout", Title: "Logout", Breadcrumb: "Logout"}, } @@ -44,6 +45,7 @@ var SettingsNavbar = []*components.Page{ GetPageByTitle(SettingsPages, "Healthchecks"), GetPageByTitle(SettingsPages, "Cronjobs"), GetPageByTitle(SettingsPages, "Workers"), + GetPageByTitle(SettingsPages, "Notifications"), GetPageByTitle(SettingsPages, "Temporal"), GetPageByTitle(SettingsPages, "Logout"), } diff --git a/internal/handlers/settingsworkers.go b/internal/handlers/settingsworkers.go index 459358b..769ed66 100644 --- a/internal/handlers/settingsworkers.go +++ b/internal/handlers/settingsworkers.go @@ -14,6 +14,11 @@ import ( "github.com/labstack/echo/v4" ) +type WorkerWithToken struct { + *models.Worker + Token string +} + type SettingsWorkers struct { *Settings Workers []*models.Worker @@ -22,7 +27,7 @@ type SettingsWorkers struct { type SettingsWorker struct { *Settings - Worker *models.Worker + Worker *WorkerWithToken } func (h *BaseHandler) SettingsWorkersGET(c echo.Context) error { @@ -54,7 +59,13 @@ func (h *BaseHandler) SettingsWorkersDescribeGET(c echo.Context) error { return err } - return c.Render(http.StatusOK, "setting_workers_describe.tmpl", &SettingsWorker{ + // Allow write access to default namespace + token, err := jwt.NewTokenForWorker(h.config.Jwt.PrivateKey, h.config.Jwt.PublicKey, worker) + if err != nil { + return err + } + + return c.Render(http.StatusOK, "settings_workers_describe.tmpl", &SettingsWorker{ Settings: NewSettings( cc.Principal.User, GetPageByTitle(SettingsPages, "Workers"), @@ -66,7 +77,10 @@ func (h *BaseHandler) SettingsWorkersDescribeGET(c echo.Context) error { Breadcrumb: worker.Name, }, }), - Worker: worker, + Worker: &WorkerWithToken{ + Worker: worker, + Token: token, + }, }) } @@ -108,20 +122,3 @@ func (h *BaseHandler) SettingsWorkersCreatePOST(c echo.Context) error { return c.Redirect(http.StatusSeeOther, "/settings/workers") } - -func (h *BaseHandler) SettingsWorkersTokenGET(c echo.Context) error { - slug := c.Param("slug") - - worker, err := services.GetWorker(context.Background(), h.query, slug) - if err != nil { - return err - } - - // Allow write access to default namespace - token, err := jwt.NewTokenForWorker(h.config.Jwt.PrivateKey, h.config.Jwt.PublicKey, worker) - if err != nil { - return err - } - - return c.JSON(http.StatusOK, map[string]string{"token": token}) -} diff --git a/internal/jwt/jwt.go b/internal/jwt/jwt.go index 9a05065..decce98 100644 --- a/internal/jwt/jwt.go +++ b/internal/jwt/jwt.go @@ -35,7 +35,6 @@ func JwtPublicKey(publicKey string) (*rsa.PublicKey, error) { type Claims struct { jwt.RegisteredClaims Permissions []string `json:"permissions"` - WorkerGroup string `json:"group"` } func NewTokenForUser(privateKey string, publicKey string, email string) (string, error) { @@ -50,7 +49,6 @@ func NewTokenForUser(privateKey string, publicKey string, email string) (string, }, // Ref: https://docs.temporal.io/self-hosted-guide/security#authorization []string{"temporal-system:admin", "default:admin"}, - "", } return NewToken(privateKey, publicKey, claims) @@ -68,7 +66,6 @@ func NewTokenForServer(privateKey string, publicKey string) (string, error) { }, // Ref: https://docs.temporal.io/self-hosted-guide/security#authorization []string{"temporal-system:admin", "default:admin"}, - "", } return NewToken(privateKey, publicKey, claims) @@ -86,7 +83,6 @@ func NewTokenForWorker(privateKey string, publicKey string, worker *models.Worke }, // Ref: https://docs.temporal.io/self-hosted-guide/security#authorization []string{"default:read", "default:write", "default:worker"}, - worker.Group, } return NewToken(privateKey, publicKey, claims) diff --git a/pkg/server/server.go b/pkg/server/server.go index ff8a80b..1cd230e 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -86,7 +86,6 @@ func (s *Server) Start() error { settings.GET("/workers/create", h.SettingsWorkersCreateGET) settings.POST("/workers/create", h.SettingsWorkersCreatePOST) settings.GET("/workers/:slug", h.SettingsWorkersDescribeGET) - settings.GET("/workers/:slug/token", h.SettingsWorkersTokenGET) settings.Match([]string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"}, "/temporal*", h.Temporal) // OAuth2 diff --git a/pkg/worker/worker.go b/pkg/worker/worker.go index 5af2f64..7e2b1ef 100644 --- a/pkg/worker/worker.go +++ b/pkg/worker/worker.go @@ -35,6 +35,14 @@ func getConnectionConfig(token string, apiUrl string) (*ConnectionConfig, error) return nil, errors.Wrap(err, "failed to connect to API") } + if res.StatusCode == http.StatusUnauthorized { + panic("WORKER_TOKEN is invalid. Either it expired or the worker was removed!") + } + + if res.StatusCode != http.StatusOK { + return nil, errors.Errorf("unexpected status code: %d", res.StatusCode) + } + body, err := io.ReadAll(res.Body) if err != nil { return nil, errors.Wrap(err, "failed to read response body") diff --git a/web/static/css/main.css b/web/static/css/main.css index 663b447..32b26ea 100644 --- a/web/static/css/main.css +++ b/web/static/css/main.css @@ -47,10 +47,6 @@ @apply bg-blue-700 text-white; } -.breadcrumb { - @apply flex mb-4; -} - .healthchecks { @apply grid justify-items-stretch justify-stretch items-center mt-20 bg-white shadow-md p-5 rounded-lg; } @@ -70,3 +66,28 @@ .healthchecks .time-range > a:last-child { @apply rounded-e-lg; } + +.settings { + @apply grid grid-cols-1 gap-5 grid-rows-[min-content] h-fit; +} +.settings section { + @apply relative overflow-x-auto shadow-md sm:rounded-lg text-gray-500 bg-white; +} +.settings section h2 { + @apply flex flex-col mb-5 text-lg font-semibold text-gray-900; +} +.settings section h2 span { + @apply text-sm font-medium text-gray-500; +} +.settings section form { + @apply grid gap-4 md:grid-cols-[2fr_1fr]; +} +.settings section form input { + @apply h-min bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5; +} +.settings section form label { + @apply col-span-2 block text-sm font-medium text-gray-900; +} +.settings section form p { + @apply text-sm font-normal text-gray-500; +} diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css index 46b0020..b7c7eaa 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -590,8 +590,8 @@ video { } } -.relative { - position: relative; +.col-span-1 { + grid-column: span 1 / span 1; } .col-span-2 { @@ -616,10 +616,6 @@ video { margin-bottom: 1rem; } -.mb-5 { - margin-bottom: 1.25rem; -} - .mb-8 { margin-bottom: 2rem; } @@ -632,6 +628,10 @@ video { margin-left: 0.25rem; } +.mr-1 { + margin-right: 0.25rem; +} + .mt-1 { margin-top: 0.25rem; } @@ -732,10 +732,6 @@ video { max-width: 1280px; } -.max-w-sm { - max-width: 24rem; -} - .flex-auto { flex: 1 1 auto; } @@ -752,6 +748,10 @@ video { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.grid-cols-\[auto_min-content\] { + grid-template-columns: auto min-content; +} + .flex-col { flex-direction: column; } @@ -1047,6 +1047,11 @@ video { color: rgb(37 99 235 / var(--tw-text-opacity)); } +.text-blue-700 { + --tw-text-opacity: 1; + color: rgb(29 78 216 / var(--tw-text-opacity)); +} + .text-gray-400 { --tw-text-opacity: 1; color: rgb(156 163 175 / var(--tw-text-opacity)); @@ -1101,18 +1106,17 @@ video { text-decoration-line: underline; } -.shadow-md { - --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - .shadow-sm { --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } +.blur { + --tw-blur: blur(8px); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + .navbar { margin-top: 2.5rem; display: flex; @@ -1233,11 +1237,6 @@ video { color: rgb(255 255 255 / var(--tw-text-opacity)); } -.breadcrumb { - margin-bottom: 1rem; - display: flex; -} - .healthchecks { margin-top: 5rem; display: grid; @@ -1308,6 +1307,106 @@ video { border-end-end-radius: 0.5rem; } +.settings { + display: grid; + height: -moz-fit-content; + height: fit-content; + grid-template-columns: repeat(1, minmax(0, 1fr)); + grid-template-rows: min-content; + gap: 1.25rem; +} + +.settings section { + position: relative; + overflow-x: auto; + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +@media (min-width: 640px) { + .settings section { + border-radius: 0.5rem; + } +} + +.settings section h2 { + margin-bottom: 1.25rem; + display: flex; + flex-direction: column; + font-size: 1.125rem; + line-height: 1.75rem; + font-weight: 600; + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.settings section h2 span { + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 500; + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} + +.settings section form { + display: grid; + gap: 1rem; +} + +@media (min-width: 768px) { + .settings section form { + grid-template-columns: 2fr 1fr; + } +} + +.settings section form input { + display: block; + height: -moz-min-content; + height: min-content; + width: 100%; + border-radius: 0.5rem; + border-width: 1px; + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity)); + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); + padding: 0.625rem; + font-size: 0.875rem; + line-height: 1.25rem; + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.settings section form input:focus { + --tw-border-opacity: 1; + border-color: rgb(59 130 246 / var(--tw-border-opacity)); + --tw-ring-opacity: 1; + --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity)); +} + +.settings section form label { + grid-column: span 2 / span 2; + display: block; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 500; + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.settings section form p { + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 400; + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} + .odd\:bg-white:nth-child(odd) { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); @@ -1347,11 +1446,6 @@ video { text-decoration-line: underline; } -.focus\:border-blue-500:focus { - --tw-border-opacity: 1; - border-color: rgb(59 130 246 / var(--tw-border-opacity)); -} - .focus\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; @@ -1368,11 +1462,6 @@ video { --tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity)); } -.focus\:ring-blue-500:focus { - --tw-ring-opacity: 1; - --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity)); -} - @media (min-width: 640px) { .sm\:w-auto { width: auto; @@ -1392,10 +1481,6 @@ video { margin-bottom: calc(0px * var(--tw-space-y-reverse)); } - .sm\:rounded-lg { - border-radius: 0.5rem; - } - .sm\:px-8 { padding-left: 2rem; padding-right: 2rem; @@ -1403,10 +1488,6 @@ video { } @media (min-width: 768px) { - .md\:grid-cols-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - .md\:text-3xl { font-size: 1.875rem; line-height: 2.25rem; @@ -1414,8 +1495,8 @@ video { } @media (min-width: 1024px) { - .lg\:grid-cols-\[min-content_auto\] { - grid-template-columns: min-content auto; + .lg\:grid-cols-\[min-content_minmax\(0\2c 1fr\)\] { + grid-template-columns: min-content minmax(0,1fr); } .lg\:px-40 { diff --git a/web/templates/components/settings.tmpl b/web/templates/components/settings.tmpl index 5d58fb6..8431674 100644 --- a/web/templates/components/settings.tmpl +++ b/web/templates/components/settings.tmpl @@ -6,7 +6,7 @@ {{ $path = .SettingsSidebarActive.Path }} {{ end }} -
+
-
-