feat(settings): improve overall design

This commit is contained in:
Tine 2024-02-23 12:18:02 +01:00
parent cd7abb7e33
commit 3d7fb901d0
Signed by: mentos1386
SSH key fingerprint: SHA256:MNtTsLbihYaWF8j1fkOHfkKNlnN1JQfxEU/rBU8nCGw
19 changed files with 428 additions and 263 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@
{{ $path = .SettingsSidebarActive.Path }}
{{ end }}
<div class="container max-w-screen-lg mt-20 grid grid-cols-1 lg:grid-cols-[min-content_auto] gap-8">
<div class="container max-w-screen-lg mt-20 grid grid-cols-1 lg:grid-cols-[min-content_minmax(0,1fr)] gap-8">
<ul class="sidebar">
{{range .SettingsSidebar}}
<li>
@ -20,8 +20,8 @@
</li>
{{end}}
</ul>
<div>
<nav class="breadcrumb" aria-label="Breadcrumb">
<div class="settings">
<nav aria-label="Breadcrumb">
<ol class="inline-flex items-center">
{{ range .SettingsBreadcrumbs }}
<li class="inline-flex items-center">

View file

@ -1,5 +1,5 @@
{{define "main"}}
<div class="text-center">
<div class="text-center mt-20">
<h1 class="text-3xl mb-4 font-bold"><span class="text-red-600">Error 404:</span> Page was not found!</h1>
<p>We didn't find the page you were looking for. Please check the URL and try again.</p>
<p>Or you can go back to the <a class="underline text-blue-600" href="/">homepage</a>.</p>

View file

@ -3,24 +3,22 @@
{{ $description := "Healthchecks represent periodicly the k6 script, to see if the monitored service is healthy." }}
{{ if eq .HealthchecksLength 0 }}
<section>
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16">
<h1 class="mb-4 text-2xl font-extrabold tracking-tight leading-none text-gray-900 md:text-3xl lg:text-4xl">
There are no healthchecks yet.
</h1>
<p class="mb-8 text-l font-normal text-gray-500 lg:text-l sm:px-8 lg:px-40">
{{ $description }}
</p>
<div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
<a href="/settings/healthchecks/create" class="inline-flex justify-center items-center py-3 px-5 text-base font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300">
Create First Healthcheck
<svg class="feather ml-1 h-5 w-5 overflow-visible"><use href="/static/icons/feather-sprite.svg#plus" /></svg>
</a>
</div>
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16">
<h1 class="mb-4 text-2xl font-extrabold tracking-tight leading-none text-gray-900 md:text-3xl lg:text-4xl">
There are no healthchecks yet.
</h1>
<p class="mb-8 text-l font-normal text-gray-500 lg:text-l sm:px-8 lg:px-40">
{{ $description }}
</p>
<div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
<a href="/settings/healthchecks/create" class="inline-flex justify-center items-center py-3 px-5 text-base font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300">
Create First Healthcheck
<svg class="feather ml-1 h-5 w-5 overflow-visible"><use href="/static/icons/feather-sprite.svg#plus" /></svg>
</a>
</div>
</section>
</div>
{{ else }}
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<section>
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<caption class="p-5 text-lg font-semibold text-left rtl:text-right text-gray-900 bg-white">
List of Healthchecks
@ -77,6 +75,6 @@
</tbody>
{{end}}
</table>
</div>
</section>
{{end}}
{{end}}

View file

@ -1,27 +1,23 @@
{{define "settings"}}
<section class="relative overflow-x-auto shadow-md sm:rounded-lg p-5 text-gray-500 bg-white">
<h1 class="text-lg font-semibold text-gray-900">
Creating new Healthcheck.
</h1>
<form class="mt-4" action="/settings/healthchecks/create" method="post">
<div class="mb-5">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">Name</label>
<input type="name" name="name" id="name" placeholder="Github.com" class="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"/>
</div>
<div class="mb-5">
<label for="workergroups" class="block mb-2 text-sm font-medium text-gray-900">Worker Groups</label>
<input type="text" name="workergroups" id="workergroups" placeholder="europe,asia" class="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"/>
</div>
<div class="mb-5">
<label for="schedule" class="block mb-2 text-sm font-medium text-gray-900">Schedule</label>
<input type="text" name="schedule" id="schedule" placeholder="* * * * *" class="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"/>
</div>
<div class="mb-5">
<label for="script" class="block mb-2 text-sm font-medium text-gray-900">Script</label>
<input required type="hidden" id="script" name="script">
<div id="editor" class="block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"></div>
</div>
<button type="submit" onclick="save()" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center">Create</button>
<section class="p-5">
<form action="/settings/healthchecks/create" method="post">
<label for="name">Name</label>
<input type="name" name="name" id="name" placeholder="Github.com">
<p> Name of the healthcheck can be anything.</p>
<label for="workergroups">Worker Groups</label>
<input type="text" name="workergroups" id="workergroups" placeholder="NA EU"/>
<p> Worker groups are used to distribute the healthcheck to specific workers.</p>
<label for="schedule">Schedule</label>
<input type="text" name="schedule" id="schedule" placeholder="* * * * *"/>
<p> Schedule is a cron expression that defines when the healthcheck should be executed.</p>
<label for="script">Script</label>
<input required type="hidden" id="script" name="script">
<div id="editor" class="block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"></div>
<p>
Script is what determines the status of a service.
You can read more about it on <a target="_blank" href="https://k6.io/docs/using-k6/http-requests/" class="text-blue-700">k6 documentation</a>.
</p>
<button type="submit" onclick="save()" class="col-span-2 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center">Create</button>
</form>
</section>

View file

@ -1,59 +1,63 @@
{{define "settings"}}
<section class="relative overflow-x-auto shadow-md sm:rounded-lg p-5 text-gray-500 bg-white">
<section class="p-5">
<form action="/settings/healthchecks/{{ .Healthcheck.Slug }}" method="post">
<div class="grid md:grid-cols-2">
<h1 class="text-2xl font-semibold text-gray-900">
{{ .Healthcheck.Name }}
</h1>
<button type="submit" onclick="save()" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center inline-flex justify-self-end">Save</button>
</div>
<div class="mb-5">
<label for="workergroups" class="block mb-2 text-sm font-medium text-gray-900">Worker Groups</label>
<input type="text" name="workergroups" id="workergroups" value="{{ StringsJoin .Healthcheck.WorkerGroups " " }}" class="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"/>
</div>
<div class="mb-5">
<label for="schedule" class="block mb-2 text-sm font-medium text-gray-900">Schedule</label>
<input type="text" name="schedule" id="schedule" value="{{ .Healthcheck.Schedule }}" class="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"/>
</div>
<div class="mb-5">
<label for="script" class="block mb-2 text-sm font-medium text-gray-900">Script</label>
<input required type="hidden" id="script" name="script">
<div id="editor" class="block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"></div>
</div>
<h2>
Configuration
</h2>
<button type="submit" onclick="save()" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center inline-flex justify-self-end">Save</button>
<label for="workergroups">Worker Groups</label>
<input type="text" name="workergroups" id="workergroups" value="{{ StringsJoin .Healthcheck.WorkerGroups " " }}"/>
<p> Worker groups are used to distribute the healthcheck to specific workers.</p>
<label for="schedule">Schedule</label>
<input type="text" name="schedule" id="schedule" value="{{ .Healthcheck.Schedule }}"/>
<p> Schedule is a cron expression that defines when the healthcheck should be executed.</p>
<label for="script">Script</label>
<input required type="hidden" id="script" name="script">
<div id="editor" class="block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"></div>
<p>
Script is what determines the status of a service.
You can read more about it on <a target="_blank" href="https://k6.io/docs/using-k6/http-requests/" class="text-blue-700">k6 documentation</a>.
</p>
</form>
<div>
<h2 class="text-lg font-semibold text-gray-900">History</h2>
<table class="min-w-full">
<thead>
<tr>
<th class="px-6 py-3 border-b-2 border-gray-300 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 border-b-2 border-gray-300 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Created At</th>
<th class="px-6 py-3 border-b-2 border-gray-300 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Duration</th>
<th class="px-6 py-3 border-b-2 border-gray-300 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Note</th>
</tr>
</thead>
<tbody>
{{range .Healthcheck.History}}
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{if eq .Status "SUCCESS"}}bg-green-100 text-green-800{{else}}bg-red-100 text-red-800{{end}}">
{{ .Status }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{{ .CreatedAt.Format "2006-01-02 15:04:05" }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
{ .Duration }
</td>
<td class="px-6 py-4">
{{ .Note }}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</section>
<section>
<table class="min-w-full">
<caption class="p-5 text-lg font-semibold text-left rtl:text-right text-gray-900 bg-white">
History
<p class="mt-1 text-sm font-normal text-gray-500">
Last executions of healthcheck script.
</p>
</caption>
<thead>
<tr>
<th class="px-6 py-3 border-b-2 border-gray-300 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 border-b-2 border-gray-300 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Created At</th>
<th class="px-6 py-3 border-b-2 border-gray-300 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Duration</th>
<th class="px-6 py-3 border-b-2 border-gray-300 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Note</th>
</tr>
</thead>
<tbody>
{{range .Healthcheck.History}}
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{if eq .Status "SUCCESS"}}bg-green-100 text-green-800{{else}}bg-red-100 text-red-800{{end}}">
{{ .Status }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{{ .CreatedAt.Format "2006-01-02 15:04:05" }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
{ .Duration }
</td>
<td class="px-6 py-4">
{{ .Note }}
</td>
</tr>
{{end}}
</tbody>
</table>
</section>
<script src="/static/monaco/vs/loader.js"></script>

View file

@ -1,14 +1,10 @@
{{define "settings"}}
<section>
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16">
<h1 class="mb-4 text-2xl font-extrabold tracking-tight leading-none text-gray-900 md:text-3xl lg:text-4xl">
Hi there, {{.User.Email}}.
</h1>
<p class="mb-8 text-l font-normal text-gray-500 lg:text-l sm:px-8 lg:px-40">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</p>
</div>
</section>
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16">
<h1 class="mb-4 text-2xl font-extrabold tracking-tight leading-none text-gray-900 md:text-3xl lg:text-4xl">
Hi there, {{.User.Email}}.
</h1>
<p class="mb-8 text-l font-normal text-gray-500 lg:text-l sm:px-8 lg:px-40">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</p>
</div>
{{end}}

View file

@ -3,72 +3,70 @@
{{ $description := "Workers are executing healthchecks. You can deploy multiple of thems to multiple regions for wider coverage." }}
{{ if eq .WorkersLength 0 }}
<section>
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16">
<h1 class="mb-4 text-2xl font-extrabold tracking-tight leading-none text-gray-900 md:text-3xl lg:text-4xl">
There are no workers yet.
</h1>
<p class="mb-8 text-l font-normal text-gray-500 lg:text-l sm:px-8 lg:px-40">
{{ $description }}
</p>
<div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
<a href="/settings/workers/create" class="inline-flex justify-center items-center py-3 px-5 text-base font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300">
Create First Worker
<svg class="feather ml-1 h-5 w-5 overflow-visible"><use href="/static/icons/feather-sprite.svg#plus" /></svg>
</a>
</div>
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16">
<h1 class="mb-4 text-2xl font-extrabold tracking-tight leading-none text-gray-900 md:text-3xl lg:text-4xl">
There are no workers yet.
</h1>
<p class="mb-8 text-l font-normal text-gray-500 lg:text-l sm:px-8 lg:px-40">
{{ $description }}
</p>
<div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
<a href="/settings/workers/create" class="inline-flex justify-center items-center py-3 px-5 text-base font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300">
Create First Worker
<svg class="feather ml-1 h-5 w-5 overflow-visible"><use href="/static/icons/feather-sprite.svg#plus" /></svg>
</a>
</div>
</section>
{{ else }}
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<caption class="p-5 text-lg font-semibold text-left rtl:text-right text-gray-900 bg-white">
List of Workers
<div class="mt-1 gap-4 flex justify-between">
<p class="mt-1 text-sm font-normal text-gray-500">
{{ $description }}
</p>
<a href="/settings/workers/create" class="inline-flex justify-center items-center py-1 px-2 text-sm font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300">
Create New
<svg class="feather h-5 w-5 overflow-visible"><use href="/static/icons/feather-sprite.svg#plus" /></svg>
</a>
</div>
</caption>
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3">
Name
</th>
<th scope="col" class="px-6 py-3">
Group
</th>
<th scope="col" class="px-6 py-3">
Status
</th>
<th scope="col" class="px-6 py-3">
Action
</th>
</tr>
</thead>
{{range .Workers}}
<tbody>
<tr class="odd:bg-white even:bg-gray-50">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
{{.Name}}
</th>
<td class="px-6 py-4">
{{.Group}}
</td>
<td class="px-6 py-4">
OK
</td>
<td class="px-6 py-4">
<a href="/settings/workers/{{.Slug}}" class="font-medium text-blue-600 hover:underline">Details</a>
</td>
</tr>
</tbody>
{{end}}
</table>
</div>
{{ else }}
<section>
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<caption class="p-5 text-lg font-semibold text-left rtl:text-right text-gray-900 bg-white">
List of Workers
<div class="mt-1 gap-4 flex justify-between">
<p class="mt-1 text-sm font-normal text-gray-500">
{{ $description }}
</p>
<a href="/settings/workers/create" class="inline-flex justify-center items-center py-1 px-2 text-sm font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300">
Create New
<svg class="feather h-5 w-5 overflow-visible"><use href="/static/icons/feather-sprite.svg#plus" /></svg>
</a>
</div>
</caption>
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3">
Name
</th>
<th scope="col" class="px-6 py-3">
Group
</th>
<th scope="col" class="px-6 py-3">
Status
</th>
<th scope="col" class="px-6 py-3">
Action
</th>
</tr>
</thead>
{{range .Workers}}
<tbody>
<tr class="odd:bg-white even:bg-gray-50">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
{{.Name}}
</th>
<td class="px-6 py-4">
{{.Group}}
</td>
<td class="px-6 py-4">
OK
</td>
<td class="px-6 py-4">
<a href="/settings/workers/{{.Slug}}" class="font-medium text-blue-600 hover:underline">Details</a>
</td>
</tr>
</tbody>
{{end}}
</table>
</section>
{{end}}
{{end}}

View file

@ -1,18 +1,17 @@
{{define "settings"}}
<section class="relative overflow-x-auto shadow-md sm:rounded-lg p-5 text-gray-500 bg-white">
<h1 class="text-lg font-semibold text-gray-900">
Creating new worker.
</h1>
<form class="max-w-sm mt-4" action="/settings/workers/create" method="post">
<div class="mb-5">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">Name</label>
<input type="text" name="name" id="name" placeholder="FooBar" class="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"/>
</div>
<div class="mb-5">
<label for="group" class="block mb-2 text-sm font-medium text-gray-900">Group</label>
<input type="text" name="group" id="group" placeholder="Europe" class="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"/>
</div>
<button type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center">Create</button>
<section class="p-5">
<form action="/settings/workers/create" method="post">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">Name</label>
<input type="text" name="name" id="name" placeholder="aws-eu-central-1"/>
<p>Worker name can be anything.</p>
<label for="group" class="block mb-2 text-sm font-medium text-gray-900">Group</label>
<input type="text" name="group" id="group" placeholder="EU"/>
<p>
Group is used to distinguish between different workers.
For example, you can have a group for different regions,
different datacenters or different environments.
</p>
<button type="submit" class="col-span-2 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center">Create</button>
</form>
</section>
{{end}}

View file

@ -0,0 +1,51 @@
{{define "settings"}}
<section class="p-5">
<form action="/settings/workers/{{ .Worker.Slug }}" method="post">
<h2>
Configuration
</h2>
<button type="submit" onclick="save()" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center inline-flex justify-self-end">Save</button>
<label for="group">Group</label>
<input type="text" name="group" id="group" value="{{ .Worker.Group }}"/>
<p>
Group is used to distinguish between different workers.
For example, you can have a group for different regions,
different datacenters or different environments.
</p>
</form>
</section>
<section class="p-5">
<h2>
Token
<span>Use it as <code>WORKER_TOKEN</code> configuration option.</span>
</h2>
<div class="grid grid-cols-[auto_min-content] gap-2">
<pre id="token" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5 overflow-x-auto">{{ .Worker.Token }}</pre>
<button id="copy-token" data-copy-to-clipboard-target="npm-install" class="col-span-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto p-2.5 text-center items-center inline-flex justify-center">
<span id="default-message">Copy</span>
<span id="success-message" class="hidden inline-flex items-center">
<svg class="feather h-4 w-4 mr-1 overflow-visible"><use href="/static/icons/feather-sprite.svg#check"/></svg>
Copied!
</span>
</button>
</div>
</section>
<script>
const copyTokenButton = document.getElementById('copy-token');
copyTokenButton.addEventListener('click', function() {
this.blur();
const copyText = document.getElementById('token');
navigator.clipboard.writeText(copyText.innerText);
const defaultMessage = document.getElementById('default-message');
const successMessage = document.getElementById('success-message');
defaultMessage.classList.add('hidden');
successMessage.classList.remove('hidden');
setTimeout(() => {
defaultMessage.classList.remove('hidden');
successMessage.classList.add('hidden');
}, 1500);
});
</script>
{{end}}

View file

@ -44,6 +44,7 @@ func NewTemplates() *Templates {
"settings_overview.tmpl": loadSettings("pages/settings_overview.tmpl"),
"settings_workers.tmpl": loadSettings("pages/settings_workers.tmpl"),
"settings_workers_create.tmpl": loadSettings("pages/settings_workers_create.tmpl"),
"settings_workers_describe.tmpl": loadSettings("pages/settings_workers_describe.tmpl"),
"settings_healthchecks.tmpl": loadSettings("pages/settings_healthchecks.tmpl"),
"settings_healthchecks_create.tmpl": loadSettings("pages/settings_healthchecks_create.tmpl"),
"settings_healthchecks_describe.tmpl": loadSettings("pages/settings_healthchecks_describe.tmpl"),