detached 23c7aaa

Refactor action response handling and enhance UI toggle states

Backend:
- Refactor common action responses to use a centralized function ActionSuccess.
- Modify HTTP responses to use ActionSuccess in multiple handlers.

Frontend:
- Implement Hyperscript for toggle state management in upload and create-container buttons.
- Update HMTX target to a common div for all action responses.
- Remove redundant JavaScript for toggle states and element removal.
- Update libraries and fix broken links in header.
commit
23c7aaa0091fa6fcdaf4fb5e775d5c93a4738519
author
bnema · · 2023-10-20 16:32 UTC
parent
c46ba10
signature
unsigned
15 files · +3907 -134
M internal/httpserve/handler/containers.go +20 -8
@@ -7,6 +7,7 @@ import (
77 "time"
88
99 "github.com/bnema/gordon/internal/app"
10+ "github.com/bnema/gordon/internal/templating/load"
1011 "github.com/bnema/gordon/internal/templating/render"
1112 "github.com/bnema/gordon/pkg/docker"
1213 "github.com/bnema/gordon/pkg/humanize"
@@ -14,8 +15,10 @@ import (
1415 "github.com/labstack/echo/v4"
1516 )
1617
17-var idMap = make(map[string]string)
18-var idMapMutex sync.Mutex
18+var (
19+ idMap = make(map[string]string)
20+ idMapMutex sync.Mutex
21+)
1922
2023 type HumanReadableContainerImage struct {
2124 *types.ImageSummary
@@ -36,6 +39,16 @@ type HumanReadableContainer struct {
3639 StateColor string
3740 }
3841
42+func ActionSuccess(a *app.App) string {
43+ successFragment, err := load.Fragment(a, "success")
44+ if err != nil {
45+ fmt.Println(err)
46+ return ""
47+ }
48+
49+ return successFragment
50+}
51+
3952 // ImageManagerComponent handles the /image-manager route (HTMX route)
4053 func ImageManagerComponent(c echo.Context, a *app.App) error {
4154 images, err := docker.ListContainerImages()
@@ -103,8 +116,7 @@ func ImageManagerDelete(c echo.Context, a *app.App) error {
103116 delete(idMap, ShortID)
104117 idMapMutex.Unlock()
105118
106- // Since it is HTMX we return a HTML div with the message "Removed"
107- return c.HTML(http.StatusOK, `<div>Removed</div>`)
119+ return c.String(http.StatusOK, ActionSuccess(a))
108120 }
109121
110122 // ContainerManagerComponent handles the /container-manager route
@@ -169,7 +181,7 @@ func ContainerManagerDelete(c echo.Context, a *app.App) error {
169181 if err != nil {
170182 return sendError(c, err)
171183 }
172- return c.HTML(http.StatusOK, `Success`)
184+ return c.HTML(http.StatusOK, ActionSuccess(a))
173185 }
174186
175187 // ContainerManagerStop handles the /container-manager/stop route
@@ -182,7 +194,7 @@ func ContainerManagerStop(c echo.Context, a *app.App) error {
182194 }
183195 // if stopped is true, the container was stopped gracefully
184196 if stopped {
185- return c.HTML(http.StatusOK, `Success`)
197+ return c.HTML(http.StatusOK, ActionSuccess(a))
186198 }
187199 // If stopped is false, the container was not stopped gracefully we force stop it
188200 err = docker.StopContainerRagefully(ID)
@@ -190,7 +202,7 @@ func ContainerManagerStop(c echo.Context, a *app.App) error {
190202 return sendError(c, err)
191203 }
192204
193- return c.HTML(http.StatusOK, `Success`)
205+ return c.HTML(http.StatusOK, ActionSuccess(a))
194206 }
195207
196208 // ContainerManagerStart handles the /container-manager/start route
@@ -200,5 +212,5 @@ func ContainerManagerStart(c echo.Context, a *app.App) error {
200212 if err != nil {
201213 return sendError(c, err)
202214 }
203- return c.HTML(http.StatusOK, `Success`)
215+ return c.HTML(http.StatusOK, ActionSuccess(a))
204216 }
@@ -7,6 +7,7 @@ import (
7 "time"7 "time"
8 8
9 "github.com/bnema/gordon/internal/app"9 "github.com/bnema/gordon/internal/app"
10+ "github.com/bnema/gordon/internal/templating/load"
10 "github.com/bnema/gordon/internal/templating/render"11 "github.com/bnema/gordon/internal/templating/render"
11 "github.com/bnema/gordon/pkg/docker"12 "github.com/bnema/gordon/pkg/docker"
12 "github.com/bnema/gordon/pkg/humanize"13 "github.com/bnema/gordon/pkg/humanize"
@@ -14,8 +15,10 @@ import (
14 "github.com/labstack/echo/v4"15 "github.com/labstack/echo/v4"
15 )16 )
16 17
17-var idMap = make(map[string]string)18+var (
18-var idMapMutex sync.Mutex19+ idMap = make(map[string]string)
20+ idMapMutex sync.Mutex
21+)
19 22
20 type HumanReadableContainerImage struct {23 type HumanReadableContainerImage struct {
21 *types.ImageSummary24 *types.ImageSummary
@@ -36,6 +39,16 @@ type HumanReadableContainer struct {
36 StateColor string39 StateColor string
37 }40 }
38 41
42+func ActionSuccess(a *app.App) string {
43+ successFragment, err := load.Fragment(a, "success")
44+ if err != nil {
45+ fmt.Println(err)
46+ return ""
47+ }
48+
49+ return successFragment
50+}
51+
39 // ImageManagerComponent handles the /image-manager route (HTMX route)52 // ImageManagerComponent handles the /image-manager route (HTMX route)
40 func ImageManagerComponent(c echo.Context, a *app.App) error {53 func ImageManagerComponent(c echo.Context, a *app.App) error {
41 images, err := docker.ListContainerImages()54 images, err := docker.ListContainerImages()
@@ -103,8 +116,7 @@ func ImageManagerDelete(c echo.Context, a *app.App) error {
103 delete(idMap, ShortID)116 delete(idMap, ShortID)
104 idMapMutex.Unlock()117 idMapMutex.Unlock()
105 118
106- // Since it is HTMX we return a HTML div with the message "Removed"119+ return c.String(http.StatusOK, ActionSuccess(a))
107- return c.HTML(http.StatusOK, `<div>Removed</div>`)
108 }120 }
109 121
110 // ContainerManagerComponent handles the /container-manager route122 // ContainerManagerComponent handles the /container-manager route
@@ -169,7 +181,7 @@ func ContainerManagerDelete(c echo.Context, a *app.App) error {
169 if err != nil {181 if err != nil {
170 return sendError(c, err)182 return sendError(c, err)
171 }183 }
172- return c.HTML(http.StatusOK, `Success`)184+ return c.HTML(http.StatusOK, ActionSuccess(a))
173 }185 }
174 186
175 // ContainerManagerStop handles the /container-manager/stop route187 // ContainerManagerStop handles the /container-manager/stop route
@@ -182,7 +194,7 @@ func ContainerManagerStop(c echo.Context, a *app.App) error {
182 }194 }
183 // if stopped is true, the container was stopped gracefully195 // if stopped is true, the container was stopped gracefully
184 if stopped {196 if stopped {
185- return c.HTML(http.StatusOK, `Success`)197+ return c.HTML(http.StatusOK, ActionSuccess(a))
186 }198 }
187 // If stopped is false, the container was not stopped gracefully we force stop it199 // If stopped is false, the container was not stopped gracefully we force stop it
188 err = docker.StopContainerRagefully(ID)200 err = docker.StopContainerRagefully(ID)
@@ -190,7 +202,7 @@ func ContainerManagerStop(c echo.Context, a *app.App) error {
190 return sendError(c, err)202 return sendError(c, err)
191 }203 }
192 204
193- return c.HTML(http.StatusOK, `Success`)205+ return c.HTML(http.StatusOK, ActionSuccess(a))
194 }206 }
195 207
196 // ContainerManagerStart handles the /container-manager/start route208 // ContainerManagerStart handles the /container-manager/start route
@@ -200,5 +212,5 @@ func ContainerManagerStart(c echo.Context, a *app.App) error {
200 if err != nil {212 if err != nil {
201 return sendError(c, err)213 return sendError(c, err)
202 }214 }
203- return c.HTML(http.StatusOK, `Success`)215+ return c.HTML(http.StatusOK, ActionSuccess(a))
204 }216 }
M internal/httpserve/handler/uploadcontainerimage.go +1 -1
@@ -60,5 +60,5 @@ func UploadImagePOSTHandler(c echo.Context, a *app.App) error {
6060 if err != nil {
6161 return sendError(c, err)
6262 }
63- return c.HTML(http.StatusOK, "Success")
63+ return c.HTML(http.StatusOK, ActionSuccess(a))
6464 }
@@ -60,5 +60,5 @@ func UploadImagePOSTHandler(c echo.Context, a *app.App) error {
60 if err != nil {60 if err != nil {
61 return sendError(c, err)61 return sendError(c, err)
62 }62 }
63- return c.HTML(http.StatusOK, "Success")63+ return c.HTML(http.StatusOK, ActionSuccess(a))
64 }64 }
A internal/templating/load/loadfragment.go +28 -0
@@ -0,0 +1,28 @@
1+package load
2+
3+import (
4+ "fmt"
5+ "io"
6+
7+ "github.com/bnema/gordon/internal/app"
8+)
9+
10+// LoadFragment loads a specific HTML fragment from the app's TemplateFS
11+func Fragment(a *app.App, fragmentName string) (string, error) {
12+ filePath := fmt.Sprintf("html/fragments/%s.gohtml", fragmentName)
13+
14+ // Open the file
15+ file, err := a.TemplateFS.Open(filePath)
16+ if err != nil {
17+ return "", err
18+ }
19+ defer file.Close()
20+
21+ // Read the file's content
22+ content, err := io.ReadAll(file)
23+ if err != nil {
24+ return "", err
25+ }
26+
27+ return string(content), nil
28+}
@@ -0,0 +1,28 @@
1+package load
2+
3+import (
4+ "fmt"
5+ "io"
6+
7+ "github.com/bnema/gordon/internal/app"
8+)
9+
10+// LoadFragment loads a specific HTML fragment from the app's TemplateFS
11+func Fragment(a *app.App, fragmentName string) (string, error) {
12+ filePath := fmt.Sprintf("html/fragments/%s.gohtml", fragmentName)
13+
14+ // Open the file
15+ file, err := a.TemplateFS.Open(filePath)
16+ if err != nil {
17+ return "", err
18+ }
19+ defer file.Close()
20+
21+ // Read the file's content
22+ content, err := io.ReadAll(file)
23+ if err != nil {
24+ return "", err
25+ }
26+
27+ return string(content), nil
28+}
M internal/templating/models/html/admin/manager.gohtml +0 -3
@@ -9,9 +9,6 @@
99 <div class="flex-1 flex flex-col space-y-4 pt-16 md:pt-32 md:px-6">
1010 <div class="flex flex-col md:flex-row w-full flex-grow">
1111 <div class="flex-grow bg-white p-4 md:rounded-md md:shadow mb-4">
12- <h1 class="text-lg md:text-2xl font-bold text-gray-400 IBMPlexSans justify-center text-center">
13- {{index .Lang "manager" "content" "title"}}
14- </h1>
1512 <!-- List all the containers -->
1613 <div
1714 id="container-manager"
@@ -9,9 +9,6 @@
9 <div class="flex-1 flex flex-col space-y-4 pt-16 md:pt-32 md:px-6">9 <div class="flex-1 flex flex-col space-y-4 pt-16 md:pt-32 md:px-6">
10 <div class="flex flex-col md:flex-row w-full flex-grow">10 <div class="flex flex-col md:flex-row w-full flex-grow">
11 <div class="flex-grow bg-white p-4 md:rounded-md md:shadow mb-4">11 <div class="flex-grow bg-white p-4 md:rounded-md md:shadow mb-4">
12- <h1 class="text-lg md:text-2xl font-bold text-gray-400 IBMPlexSans justify-center text-center">
13- {{index .Lang "manager" "content" "title"}}
14- </h1>
15 <!-- List all the containers -->12 <!-- List all the containers -->
16 <div13 <div
17 id="container-manager"14 id="container-manager"
M internal/templating/models/html/fragments/containerlist.gohtml +6 -4
@@ -1,3 +1,5 @@
1+<div id="actionResponse"></div>
2+
13 <div class="flex flex-col p-4" hx-boost="true">
24 <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">
35 {{index .Lang "manager" "content" "h1-containers"}}
@@ -33,7 +35,7 @@
3335 <button
3436 id="start-button-{{.ID}}"
3537 hx-post="/htmx/container-manager/start/{{.ID}}"
36- hx-target="#container-{{.ID}}"
38+ hx-target="#actionResponse"
3739 hx-target-*="#error-message-{{.ID}}"
3840 hx-swap="outerHTML"
3941 class="py-2 px-4"
@@ -44,7 +46,7 @@
4446 <button
4547 id="edit-button-{{.ID}}"
4648 hx-get="/htmx/container-manager/edit/{{.ID}}"
47- hx-target="#edit-container-{{.ID}}"
49+ hx-target="#actionResponse"
4850 hx-target-*="#error-message-{{.ID}}"
4951 hx-swap="innerHTML"
5052 class="py-2 px-4"
@@ -56,7 +58,7 @@
5658 <button
5759 id="stop-button-{{.ID}}"
5860 hx-post="/htmx/container-manager/stop/{{.ID}}"
59- hx-target="#container-{{.ID}}"
61+ hx-target="#actionResponse"
6062 hx-target-*="#error-message-{{.ID}}"
6163 hx-swap="innerHTML"
6264 hx-confirm="{{index $Lang "manager" "content" "confirm-container-stop"}}"
@@ -68,7 +70,7 @@
6870 <button
6971 id="remove-button-img-{{.ID}}"
7072 hx-delete="/htmx/container-manager/delete/{{.ID}}"
71- hx-target="#container-{{.ID}}"
73+ hx-target="#actionResponse"
7274 hx-target-*="#error-message-{{.ID}}"
7375 hx-swap="innerHTML"
7476 hx-confirm="{{index $Lang "manager" "content" "confirm-delete"}}"
@@ -1,3 +1,5 @@
1+<div id="actionResponse"></div>
2+
1 <div class="flex flex-col p-4" hx-boost="true">3 <div class="flex flex-col p-4" hx-boost="true">
2 <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">4 <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">
3 {{index .Lang "manager" "content" "h1-containers"}}5 {{index .Lang "manager" "content" "h1-containers"}}
@@ -33,7 +35,7 @@
33 <button35 <button
34 id="start-button-{{.ID}}"36 id="start-button-{{.ID}}"
35 hx-post="/htmx/container-manager/start/{{.ID}}"37 hx-post="/htmx/container-manager/start/{{.ID}}"
36- hx-target="#container-{{.ID}}"38+ hx-target="#actionResponse"
37 hx-target-*="#error-message-{{.ID}}"39 hx-target-*="#error-message-{{.ID}}"
38 hx-swap="outerHTML"40 hx-swap="outerHTML"
39 class="py-2 px-4"41 class="py-2 px-4"
@@ -44,7 +46,7 @@
44 <button46 <button
45 id="edit-button-{{.ID}}"47 id="edit-button-{{.ID}}"
46 hx-get="/htmx/container-manager/edit/{{.ID}}"48 hx-get="/htmx/container-manager/edit/{{.ID}}"
47- hx-target="#edit-container-{{.ID}}"49+ hx-target="#actionResponse"
48 hx-target-*="#error-message-{{.ID}}"50 hx-target-*="#error-message-{{.ID}}"
49 hx-swap="innerHTML"51 hx-swap="innerHTML"
50 class="py-2 px-4"52 class="py-2 px-4"
@@ -56,7 +58,7 @@
56 <button58 <button
57 id="stop-button-{{.ID}}"59 id="stop-button-{{.ID}}"
58 hx-post="/htmx/container-manager/stop/{{.ID}}"60 hx-post="/htmx/container-manager/stop/{{.ID}}"
59- hx-target="#container-{{.ID}}"61+ hx-target="#actionResponse"
60 hx-target-*="#error-message-{{.ID}}"62 hx-target-*="#error-message-{{.ID}}"
61 hx-swap="innerHTML"63 hx-swap="innerHTML"
62 hx-confirm="{{index $Lang "manager" "content" "confirm-container-stop"}}"64 hx-confirm="{{index $Lang "manager" "content" "confirm-container-stop"}}"
@@ -68,7 +70,7 @@
68 <button70 <button
69 id="remove-button-img-{{.ID}}"71 id="remove-button-img-{{.ID}}"
70 hx-delete="/htmx/container-manager/delete/{{.ID}}"72 hx-delete="/htmx/container-manager/delete/{{.ID}}"
71- hx-target="#container-{{.ID}}"73+ hx-target="#actionResponse"
72 hx-target-*="#error-message-{{.ID}}"74 hx-target-*="#error-message-{{.ID}}"
73 hx-swap="innerHTML"75 hx-swap="innerHTML"
74 hx-confirm="{{index $Lang "manager" "content" "confirm-delete"}}"76 hx-confirm="{{index $Lang "manager" "content" "confirm-delete"}}"
M internal/templating/models/html/fragments/header.gohtml +2 -2
@@ -4,11 +4,11 @@
44 <title>{{ .header.meta.title }}</title>
55 <link rel="stylesheet" href="/assets/css/tailwind.css" />
66
7- <script src="/libs/htmx.min.js"></script>
7+ <script src="/libs/htmx.js"></script>
88 <script src="/libs/htmx-response-targets.js"></script>
99 <script src="/libs/htmx-debug.js"></script>
1010 <script src="/libs/htmx-sse.js"></script>
11- <script defer src="https://unpkg.com/hyperscript.org@0.9.11"></script>
11+ <script defer src="/libs/hyperscript.min.js"></script>
1212 <script defer src="/assets/js/custom.js"></script>
1313 <!-- script wait for the page to be loaded and insert <link rel="stylesheet" href="/libs/iconoir.css" /> -->
1414 <script>
@@ -4,11 +4,11 @@
4 <title>{{ .header.meta.title }}</title>4 <title>{{ .header.meta.title }}</title>
5 <link rel="stylesheet" href="/assets/css/tailwind.css" />5 <link rel="stylesheet" href="/assets/css/tailwind.css" />
6 6
7- <script src="/libs/htmx.min.js"></script>7+ <script src="/libs/htmx.js"></script>
8 <script src="/libs/htmx-response-targets.js"></script>8 <script src="/libs/htmx-response-targets.js"></script>
9 <script src="/libs/htmx-debug.js"></script>9 <script src="/libs/htmx-debug.js"></script>
10 <script src="/libs/htmx-sse.js"></script>10 <script src="/libs/htmx-sse.js"></script>
11- <script defer src="https://unpkg.com/hyperscript.org@0.9.11"></script>11+ <script defer src="/libs/hyperscript.min.js"></script>
12 <script defer src="/assets/js/custom.js"></script>12 <script defer src="/assets/js/custom.js"></script>
13 <!-- script wait for the page to be loaded and insert <link rel="stylesheet" href="/libs/iconoir.css" /> -->13 <!-- script wait for the page to be loaded and insert <link rel="stylesheet" href="/libs/iconoir.css" /> -->
14 <script>14 <script>
M internal/templating/models/html/fragments/imagelist.gohtml +5 -26
@@ -3,13 +3,14 @@
33 <div class="flex justify-between items-center mb-1">
44 <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">Images</h1>
55 <button
6+ class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
67 id="upload-button"
78 hx-get="/htmx/upload-image"
89 hx-target="#upload-image"
910 hx-swap="innerHTML"
10- data-active="false"
11- class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
11+ _="on click toggle between .visible and .hidden on #upload-image then add .hidden to #create-container"
1212 >
13+
1314 <span class="iconoir-upload text-xl align-middle"></span>
1415 Upload
1516 </button>
@@ -36,6 +37,7 @@
3637 hx-target-*="#error-message-div-{{.ID}}"
3738 hx-swap="innerHTML"
3839 class="py-2 px-4"
40+ _="on click toggle between .visible and .hidden on #create-container then add .hidden to #upload-image"
3941 >
4042 <span title="Deploy a container using this image" class="iconoir-add-circle text-xl text-blue-800 font-bold"></span>
4143 </button>
@@ -69,27 +71,4 @@
6971 </table>
7072 <div id="upload-image"></div>
7173 <div id="create-container"></div>
72-</div>
73-
74-<script>
75- // Hide upload form if add button is clicked
76- document.querySelectorAll('[id^="add-button-img-"]').forEach((button) => {
77- button.addEventListener("click", () => {
78- document.getElementById("upload-image").innerHTML = "";
79- });
80- });
81-
82- // Hide create container form if upload button is clicke
83- document.getElementById("upload-button").addEventListener("click", () => {
84- document.getElementById("create-container").innerHTML = "";
85- });
86-
87- // Remove image from list if remove button is clicked (after confirmation)
88- document.querySelectorAll('[id^="remove-button-img-"]').forEach((button) => {
89- button.addEventListener("click", () => {
90- const id = button.id.split("-")[3];
91- const tr = document.getElementById(`img-${id}`);
92- tr.style.display = "none";
93- });
94- });
95-</script>⏎̸
74+</div>⏎̸
@@ -3,13 +3,14 @@
3 <div class="flex justify-between items-center mb-1"> 3 <div class="flex justify-between items-center mb-1">
4 <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">Images</h1>4 <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">Images</h1>
5 <button5 <button
6+ class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
6 id="upload-button"7 id="upload-button"
7 hx-get="/htmx/upload-image"8 hx-get="/htmx/upload-image"
8 hx-target="#upload-image"9 hx-target="#upload-image"
9 hx-swap="innerHTML"10 hx-swap="innerHTML"
10- data-active="false"11+ _="on click toggle between .visible and .hidden on #upload-image then add .hidden to #create-container"
11- class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
12 >12 >
13+
13 <span class="iconoir-upload text-xl align-middle"></span>14 <span class="iconoir-upload text-xl align-middle"></span>
14 Upload15 Upload
15 </button>16 </button>
@@ -36,6 +37,7 @@
36 hx-target-*="#error-message-div-{{.ID}}"37 hx-target-*="#error-message-div-{{.ID}}"
37 hx-swap="innerHTML"38 hx-swap="innerHTML"
38 class="py-2 px-4"39 class="py-2 px-4"
40+ _="on click toggle between .visible and .hidden on #create-container then add .hidden to #upload-image"
39 >41 >
40 <span title="Deploy a container using this image" class="iconoir-add-circle text-xl text-blue-800 font-bold"></span>42 <span title="Deploy a container using this image" class="iconoir-add-circle text-xl text-blue-800 font-bold"></span>
41 </button>43 </button>
@@ -69,27 +71,4 @@
69 </table>71 </table>
70 <div id="upload-image"></div>72 <div id="upload-image"></div>
71 <div id="create-container"></div>73 <div id="create-container"></div>
72-</div>74+</div>⏎̸
73-
74-<script>
75- // Hide upload form if add button is clicked
76- document.querySelectorAll('[id^="add-button-img-"]').forEach((button) => {
77- button.addEventListener("click", () => {
78- document.getElementById("upload-image").innerHTML = "";
79- });
80- });
81-
82- // Hide create container form if upload button is clicke
83- document.getElementById("upload-button").addEventListener("click", () => {
84- document.getElementById("create-container").innerHTML = "";
85- });
86-
87- // Remove image from list if remove button is clicked (after confirmation)
88- document.querySelectorAll('[id^="remove-button-img-"]').forEach((button) => {
89- button.addEventListener("click", () => {
90- const id = button.id.split("-")[3];
91- const tr = document.getElementById(`img-${id}`);
92- tr.style.display = "none";
93- });
94- });
95-</script>⏎̸
A internal/templating/models/html/fragments/success.gohtml +6 -0
@@ -0,0 +1,6 @@
1+<div
2+ id="success"
3+ _="on load wait 1s then transition opacity to 0 then remove me then wait 1s then call window.location.reload()"
4+ class="flex text-blue-800 content-center justify-center items-center w-full h-full">
5+ <p class="text-2xl text-center">Success!</p>
6+</div>
@@ -0,0 +1,6 @@
1+<div
2+ id="success"
3+ _="on load wait 1s then transition opacity to 0 then remove me then wait 1s then call window.location.reload()"
4+ class="flex text-blue-800 content-center justify-center items-center w-full h-full">
5+ <p class="text-2xl text-center">Success!</p>
6+</div>
M internal/templating/models/html/fragments/uploadimage.gohtml +6 -10
@@ -2,8 +2,7 @@
22 <div
33 class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow"
44 hx-ext="response-targets"
5- hx-boost="true"
6->
5+ hx-boost="true">
76 <div class="flex-grow bg-slate-100 p-4 rounded-md">
87 <h2>Upload Image Containers</h2>
98 <!-- Upload Form -->
@@ -14,25 +13,22 @@
1413 hx-post="/htmx/upload-image"
1514 hx-swap="innerHTML"
1615 hx-target="#image-manager"
17- hx-target-*="#error-message-div"
18- >
16+ hx-target-*="#error-message-div">
1917 <div
2018 class="upload-zone w-full h-44 bg-slate-200 rounded-md p-4"
21- hx-drop="true"
22- >
19+ hx-drop="true">
2320 Drag & Drop your file here or
2421 <input
2522 type="file"
2623 name="file"
2724 accept=".tar"
28- class="bg-transparent outline-none"
29- />
25+ class="bg-transparent outline-none" />
3026 </div>
3127 <input
3228 type="submit"
3329 value="Upload"
34- class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
35- />
30+ _="on click toggle @disabled until htmx:afterOnLoad"
31+ class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" />
3632 </form>
3733 <div id="error-message-div" class="text-red-500"></div>
3834 </div>
@@ -2,8 +2,7 @@
2 <div2 <div
3 class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow"3 class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow"
4 hx-ext="response-targets"4 hx-ext="response-targets"
5- hx-boost="true"5+ hx-boost="true">
6->
7 <div class="flex-grow bg-slate-100 p-4 rounded-md">6 <div class="flex-grow bg-slate-100 p-4 rounded-md">
8 <h2>Upload Image Containers</h2>7 <h2>Upload Image Containers</h2>
9 <!-- Upload Form -->8 <!-- Upload Form -->
@@ -14,25 +13,22 @@
14 hx-post="/htmx/upload-image"13 hx-post="/htmx/upload-image"
15 hx-swap="innerHTML"14 hx-swap="innerHTML"
16 hx-target="#image-manager"15 hx-target="#image-manager"
17- hx-target-*="#error-message-div"16+ hx-target-*="#error-message-div">
18- >
19 <div17 <div
20 class="upload-zone w-full h-44 bg-slate-200 rounded-md p-4"18 class="upload-zone w-full h-44 bg-slate-200 rounded-md p-4"
21- hx-drop="true"19+ hx-drop="true">
22- >
23 Drag & Drop your file here or20 Drag & Drop your file here or
24 <input21 <input
25 type="file"22 type="file"
26 name="file"23 name="file"
27 accept=".tar"24 accept=".tar"
28- class="bg-transparent outline-none"25+ class="bg-transparent outline-none" />
29- />
30 </div>26 </div>
31 <input27 <input
32 type="submit"28 type="submit"
33 value="Upload"29 value="Upload"
34- class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"30+ _="on click toggle @disabled until htmx:afterOnLoad"
35- />31+ class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" />
36 </form>32 </form>
37 <div id="error-message-div" class="text-red-500"></div>33 <div id="error-message-div" class="text-red-500"></div>
38 </div>34 </div>
M internal/webui/public/assets/css/custom.css +7 -5
@@ -37,7 +37,8 @@
3737 /* Import Jetbrains Mono Nerd Font Bold */
3838 @font-face {
3939 font-family: "JetBrainsMono-Nerd-Font-Bold";
40- src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Bold.woff2") format("woff2");
40+ src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Bold.woff2")
41+ format("woff2");
4142 font-weight: bold;
4243 font-style: normal;
4344 }
@@ -84,17 +85,18 @@
8485 }
8586
8687 ::view-transition-old(slide-it) {
87- animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
88+ animation:
89+ 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
8890 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
8991 }
9092 ::view-transition-new(slide-it) {
91- animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
93+ animation:
94+ 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
9295 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
9396 }
9497
9598 .gordon-title {
9699 font-family: "LilitaOne-Regular", sans-serif;
97-
98100 }
99101
100102 .IBMPlexSans {
@@ -107,4 +109,4 @@
107109
108110 .JetBrainsMonoBoldNF {
109111 font-family: "JetBrainsMono-Nerd-Font-Bold", sans-serif;
110-}⏎̸
112+}
@@ -37,7 +37,8 @@
37 /* Import Jetbrains Mono Nerd Font Bold */37 /* Import Jetbrains Mono Nerd Font Bold */
38 @font-face {38 @font-face {
39 font-family: "JetBrainsMono-Nerd-Font-Bold";39 font-family: "JetBrainsMono-Nerd-Font-Bold";
40- src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Bold.woff2") format("woff2");40+ src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Bold.woff2")
41+ format("woff2");
41 font-weight: bold;42 font-weight: bold;
42 font-style: normal;43 font-style: normal;
43 }44 }
@@ -84,17 +85,18 @@
84 }85 }
85 86
86 ::view-transition-old(slide-it) {87 ::view-transition-old(slide-it) {
87- animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,88+ animation:
89+ 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
88 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;90 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
89 }91 }
90 ::view-transition-new(slide-it) {92 ::view-transition-new(slide-it) {
91- animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,93+ animation:
94+ 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
92 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;95 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
93 }96 }
94 97
95 .gordon-title {98 .gordon-title {
96 font-family: "LilitaOne-Regular", sans-serif;99 font-family: "LilitaOne-Regular", sans-serif;
97-
98 }100 }
99 101
100 .IBMPlexSans {102 .IBMPlexSans {
@@ -107,4 +109,4 @@
107 109
108 .JetBrainsMonoBoldNF {110 .JetBrainsMonoBoldNF {
109 font-family: "JetBrainsMono-Nerd-Font-Bold", sans-serif;111 font-family: "JetBrainsMono-Nerd-Font-Bold", sans-serif;
110-}⏎̸112+}
M internal/webui/public/assets/css/tailwind.css +31 -4
@@ -575,6 +575,18 @@ video {
575575 }
576576 }
577577
578+.visible {
579+ visibility: visible;
580+}
581+
582+.invisible {
583+ visibility: hidden;
584+}
585+
586+.collapse {
587+ visibility: collapse;
588+}
589+
578590 .fixed {
579591 position: fixed;
580592 }
@@ -708,6 +720,10 @@ video {
708720 flex-direction: column;
709721 }
710722
723+.content-center {
724+ align-content: center;
725+}
726+
711727 .items-center {
712728 align-items: center;
713729 }
@@ -1027,6 +1043,14 @@ video {
10271043 outline-offset: 2px;
10281044 }
10291045
1046+.transition {
1047+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
1048+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
1049+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
1050+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1051+ transition-duration: 150ms;
1052+}
1053+
10301054 /* Import IBM Plex Fonts */
10311055
10321056 @font-face {
@@ -1067,7 +1091,8 @@ video {
10671091 @font-face {
10681092 font-family: "JetBrainsMono-Nerd-Font-Bold";
10691093
1070- src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Bold.woff2") format("woff2");
1094+ src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Bold.woff2")
1095+ format("woff2");
10711096
10721097 font-weight: bold;
10731098
@@ -1120,12 +1145,14 @@ video {
11201145 }
11211146
11221147 ::view-transition-old(slide-it) {
1123- animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
1148+ animation:
1149+ 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
11241150 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
11251151 }
11261152
11271153 ::view-transition-new(slide-it) {
1128- animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
1154+ animation:
1155+ 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
11291156 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
11301157 }
11311158
@@ -1284,4 +1311,4 @@ video {
12841311 font-size: 3rem;
12851312 line-height: 1;
12861313 }
1287-}⏎̸
1314+}
@@ -575,6 +575,18 @@ video {
575 }575 }
576 }576 }
577 577
578+.visible {
579+ visibility: visible;
580+}
581+
582+.invisible {
583+ visibility: hidden;
584+}
585+
586+.collapse {
587+ visibility: collapse;
588+}
589+
578 .fixed {590 .fixed {
579 position: fixed;591 position: fixed;
580 }592 }
@@ -708,6 +720,10 @@ video {
708 flex-direction: column;720 flex-direction: column;
709 }721 }
710 722
723+.content-center {
724+ align-content: center;
725+}
726+
711 .items-center {727 .items-center {
712 align-items: center;728 align-items: center;
713 }729 }
@@ -1027,6 +1043,14 @@ video {
1027 outline-offset: 2px;1043 outline-offset: 2px;
1028 }1044 }
1029 1045
1046+.transition {
1047+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
1048+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
1049+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
1050+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1051+ transition-duration: 150ms;
1052+}
1053+
1030 /* Import IBM Plex Fonts */1054 /* Import IBM Plex Fonts */
1031 1055
1032 @font-face {1056 @font-face {
@@ -1067,7 +1091,8 @@ video {
1067 @font-face {1091 @font-face {
1068 font-family: "JetBrainsMono-Nerd-Font-Bold";1092 font-family: "JetBrainsMono-Nerd-Font-Bold";
1069 1093
1070- src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Bold.woff2") format("woff2");1094+ src: url("/assets/fonts/JetBrainsMonoNerd/JetBrainsMonoNerdFont-Bold.woff2")
1095+ format("woff2");
1071 1096
1072 font-weight: bold;1097 font-weight: bold;
1073 1098
@@ -1120,12 +1145,14 @@ video {
1120 }1145 }
1121 1146
1122 ::view-transition-old(slide-it) {1147 ::view-transition-old(slide-it) {
1123- animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,1148+ animation:
1149+ 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
1124 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;1150 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
1125 }1151 }
1126 1152
1127 ::view-transition-new(slide-it) {1153 ::view-transition-new(slide-it) {
1128- animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,1154+ animation:
1155+ 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
1129 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;1156 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
1130 }1157 }
1131 1158
@@ -1284,4 +1311,4 @@ video {
1284 font-size: 3rem;1311 font-size: 3rem;
1285 line-height: 1;1312 line-height: 1;
1286 }1313 }
1287-}⏎̸1314+}
M internal/webui/public/assets/js/custom.js +1 -67
@@ -1,67 +1,1 @@
1-document.body.addEventListener("htmx:afterOnLoad", function(event) {
2- if (
3- event.target.id === "image-manager" &&
4- event.detail.xhr.responseText === "Success"
5- ) {
6- setTimeout(function() {
7- location.reload();
8- }, 2000);
9- }
10-});
11-
12-function toggleButtonState(event) {
13- const button = event.currentTarget;
14- const isActive = button.getAttribute('data-active') === 'true';
15-
16- // Toggle the state
17- button.setAttribute('data-active', !isActive);
18-
19- // Get the related component based on hx-target attribute
20- const hxTarget = button.getAttribute('hx-target');
21- const relatedComponent = document.querySelector(hxTarget);
22-
23- if (relatedComponent) {
24- if (isActive) {
25- relatedComponent.style.display = 'none'; // Hide
26- } else {
27- relatedComponent.style.display = 'block'; // Show
28-
29- // Adjust textarea height if it exists within the relatedComponent
30- const textarea = relatedComponent.querySelector("#container_config");
31- if (textarea) {
32- textarea.style.height = ""; // Reset the height
33- textarea.style.height = textarea.scrollHeight + "px";
34- }
35- }
36- }
37-}
38-
39-// Attach event listeners to buttons that needs to toggle state
40-document.body.addEventListener("htmx:afterSwap", function (event) {
41- const buttons = document.querySelectorAll('[id^="edit-button-"], [id^="add-button-img-"]', '[id^="upload-button"]');
42- buttons.forEach(button => {
43- button.addEventListener('click', toggleButtonState);
44- });
45-});
46-
47-// // Attach event listeners to buttons that needs to set a full location reload
48-// document.body.addEventListener("htmx:afterSwap", function (event) {
49-// const xhr = event.detail.xhr;
50-// const statusCode = xhr.status;
51-
52-// // Check for 500 status code
53-// if (statusCode >= 500 && statusCode < 600) {
54-// // Handle the error (e.g., log it or show an alert)
55-// console.error("Server returned error: ", statusCode);
56-// return; // Do not reload the page
57-// }
58-
59-// const buttons = document.querySelectorAll('[id^="start-button-"], [id^="stop-button-"], [id^="remove-button-img-"], [id^="create-container-submit"], [id^="save-button-"]');
60-// buttons.forEach(button => {
61-// button.addEventListener('click', function(event) {
62-// setTimeout(function() {
63-// location.reload();
64-// }, 5000);
65-// });
66-// });
67-// });
1+// switched to hyperscript for the most part⏎̸
@@ -1,67 +1,1 @@
1-document.body.addEventListener("htmx:afterOnLoad", function(event) {1+// switched to hyperscript for the most part⏎̸
2- if (
3- event.target.id === "image-manager" &&
4- event.detail.xhr.responseText === "Success"
5- ) {
6- setTimeout(function() {
7- location.reload();
8- }, 2000);
9- }
10-});
11-
12-function toggleButtonState(event) {
13- const button = event.currentTarget;
14- const isActive = button.getAttribute('data-active') === 'true';
15-
16- // Toggle the state
17- button.setAttribute('data-active', !isActive);
18-
19- // Get the related component based on hx-target attribute
20- const hxTarget = button.getAttribute('hx-target');
21- const relatedComponent = document.querySelector(hxTarget);
22-
23- if (relatedComponent) {
24- if (isActive) {
25- relatedComponent.style.display = 'none'; // Hide
26- } else {
27- relatedComponent.style.display = 'block'; // Show
28-
29- // Adjust textarea height if it exists within the relatedComponent
30- const textarea = relatedComponent.querySelector("#container_config");
31- if (textarea) {
32- textarea.style.height = ""; // Reset the height
33- textarea.style.height = textarea.scrollHeight + "px";
34- }
35- }
36- }
37-}
38-
39-// Attach event listeners to buttons that needs to toggle state
40-document.body.addEventListener("htmx:afterSwap", function (event) {
41- const buttons = document.querySelectorAll('[id^="edit-button-"], [id^="add-button-img-"]', '[id^="upload-button"]');
42- buttons.forEach(button => {
43- button.addEventListener('click', toggleButtonState);
44- });
45-});
46-
47-// // Attach event listeners to buttons that needs to set a full location reload
48-// document.body.addEventListener("htmx:afterSwap", function (event) {
49-// const xhr = event.detail.xhr;
50-// const statusCode = xhr.status;
51-
52-// // Check for 500 status code
53-// if (statusCode >= 500 && statusCode < 600) {
54-// // Handle the error (e.g., log it or show an alert)
55-// console.error("Server returned error: ", statusCode);
56-// return; // Do not reload the page
57-// }
58-
59-// const buttons = document.querySelectorAll('[id^="start-button-"], [id^="stop-button-"], [id^="remove-button-img-"], [id^="create-container-submit"], [id^="save-button-"]');
60-// buttons.forEach(button => {
61-// button.addEventListener('click', function(event) {
62-// setTimeout(function() {
63-// location.reload();
64-// }, 5000);
65-// });
66-// });
67-// });
A internal/webui/public/libs/htmx.js +3789 -0 large diff (3789 lines changed): collapsed
A internal/webui/public/libs/hyperscript.min.js +1 -0 generated file: collapsed
M tailwind.config.js +4 -4
@@ -2,7 +2,7 @@
22 module.exports = {
33 content: [
44 "./internal/webui/**/*.{gohtml,go}",
5- "./internal/templates/models/html/**/*.{gohtml, go}"
5+ "./internal/templating/models/html/**/*.{gohtml, go}",
66 ],
77 theme: {
88 extend: {
@@ -10,12 +10,12 @@ module.exports = {
1010 gordonmascot: {
1111 lblue: "#007BC0",
1212 mblue: "#005293",
13- hblue: '#011E5C',
13+ hblue: "#011E5C",
1414 lbeige: "#FFDCB3",
15- mbeige: '#D7B49F',
15+ mbeige: "#D7B49F",
1616 },
1717 },
1818 },
1919 },
2020 plugins: [require("@catppuccin/tailwindcss")],
21-}⏎̸
21+};
@@ -2,7 +2,7 @@
2 module.exports = {2 module.exports = {
3 content: [3 content: [
4 "./internal/webui/**/*.{gohtml,go}",4 "./internal/webui/**/*.{gohtml,go}",
5- "./internal/templates/models/html/**/*.{gohtml, go}"5+ "./internal/templating/models/html/**/*.{gohtml, go}",
6 ],6 ],
7 theme: {7 theme: {
8 extend: {8 extend: {
@@ -10,12 +10,12 @@ module.exports = {
10 gordonmascot: {10 gordonmascot: {
11 lblue: "#007BC0",11 lblue: "#007BC0",
12 mblue: "#005293",12 mblue: "#005293",
13- hblue: '#011E5C',13+ hblue: "#011E5C",
14 lbeige: "#FFDCB3",14 lbeige: "#FFDCB3",
15- mbeige: '#D7B49F',15+ mbeige: "#D7B49F",
16 },16 },
17 },17 },
18 },18 },
19 },19 },
20 plugins: [require("@catppuccin/tailwindcss")],20 plugins: [require("@catppuccin/tailwindcss")],
21-}⏎̸21+};

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close