detached 6ccaf5c

Remove button for image-manager added, popup error message customized

commit
6ccaf5c5fb5297f57b6e73bc3f4bb43b26c2d3ad
author
bnema · · 2023-09-21 17:52 UTC
parent
2451b8a
signature
unsigned
13 files · +369 -104
M internal/gotemplate/models/html/admin/manager.gohtml +15 -6
@@ -11,19 +11,28 @@
1111 <div class="flex flex-col md:flex-row w-full flex-grow md:mt-24">
1212 <div class="flex-grow bg-white p-4 rounded-md shadow mb-4">
1313 <h1 class="text-lg md:text-2xl font-bold">Container Manager</h1>
14- <!-- List all the images on load with HTMX -->
14+ <!-- List all the containers -->
1515 <div
16- id="container-images"
17- hx-get="/htmx/image-manager"
16+ id="container-manager"
17+ hx-get="/htmx/container-manager"
1818 hx-trigger="load"
1919 hx-swap="innerHTML"
2020 hx-swap-oob="true"
21- hx-target-error="container-list-images-error"
21+ hx-target-error="container-manager-error"
2222 ></div>
23- <div id="container-list-images-error" style="display: none"></div>
23+ <div id="container-manager-error" style="display: none"></div>
24+ <!-- List all the images -->
25+ <div
26+ id="image-manager"
27+ hx-get="/htmx/image-manager"
28+ hx-trigger="load"
29+ hx-swap="innerHTML"
30+ hx-swap-oob="true"
31+ hx-target-error="image-manager-error"
32+ ></div>
33+ <div id="image-manager-error" style="display: none"></div>
2434 </div>
2535 </div>
26- </div>
2736
2837 <!-- footer -->
2938 {{template "footer.gohtml" .}}
@@ -11,19 +11,28 @@
11 <div class="flex flex-col md:flex-row w-full flex-grow md:mt-24">11 <div class="flex flex-col md:flex-row w-full flex-grow md:mt-24">
12 <div class="flex-grow bg-white p-4 rounded-md shadow mb-4">12 <div class="flex-grow bg-white p-4 rounded-md shadow mb-4">
13 <h1 class="text-lg md:text-2xl font-bold">Container Manager</h1>13 <h1 class="text-lg md:text-2xl font-bold">Container Manager</h1>
14- <!-- List all the images on load with HTMX -->14+ <!-- List all the containers -->
15 <div15 <div
16- id="container-images"16+ id="container-manager"
17- hx-get="/htmx/image-manager"17+ hx-get="/htmx/container-manager"
18 hx-trigger="load"18 hx-trigger="load"
19 hx-swap="innerHTML"19 hx-swap="innerHTML"
20 hx-swap-oob="true"20 hx-swap-oob="true"
21- hx-target-error="container-list-images-error"21+ hx-target-error="container-manager-error"
22 ></div>22 ></div>
23- <div id="container-list-images-error" style="display: none"></div>23+ <div id="container-manager-error" style="display: none"></div>
24+ <!-- List all the images -->
25+ <div
26+ id="image-manager"
27+ hx-get="/htmx/image-manager"
28+ hx-trigger="load"
29+ hx-swap="innerHTML"
30+ hx-swap-oob="true"
31+ hx-target-error="image-manager-error"
32+ ></div>
33+ <div id="image-manager-error" style="display: none"></div>
24 </div>34 </div>
25 </div>35 </div>
26- </div>
27 36
28 <!-- footer -->37 <!-- footer -->
29 {{template "footer.gohtml" .}}38 {{template "footer.gohtml" .}}
A internal/gotemplate/models/html/fragments/containerlist.gohtml +46 -0
@@ -0,0 +1,46 @@
1+<div class="flex flex-col p-4">
2+ <h1 class="text-lg md:text-xl font-bold">Running containers</h1>
3+ <div class="flex font-bold border-t-2 align-baseline py-2">
4+ <div class="w-1/6">Name</div>
5+ <div class="w-1/6">Command</div>
6+ <div class="w-1/6">Created</div>
7+ <div class="w-1/6">Status</div>
8+ <div class="w-1/6">Port</div>
9+ <div class="w-1/6">State</div>
10+ <div class="w-1/4">Action</div>
11+ </div>
12+ {{ range.Containers }}
13+ <div id="{{.ID}}" class="flex py-2 align-baseline justify-center">
14+ <div class="w-1/6">{{ range.Names }}{{.}}<br />{{ end }}</div>
15+ <div class="w-1/6" style="text-decoration: underline">{{.Command}}</div>
16+ <div class="w-1/6">{{.CreatedStr}}</div>
17+ <div class="w-1/6">{{.Status}} (Up since {{.UpSince}})</div>
18+ <div class="w-1/6">{{ range.Ports }}{{.}}<br />{{ end }}</div>
19+ <div class="w-1/6" style="color: {{.StateColor}};">{{.State}}</div>
20+ <div class="w-1/4">
21+ <div x-data="{ success: false, loading: false }">
22+ <button
23+ id="remove-button-{{.ShortID}}"
24+ x-bind:class="{ 'bg-green-500': success, 'bg-red-500': !success, 'text-white': true, 'px-2': true, 'py-1': true, 'rounded': true }"
25+ @click="loading = true"
26+ hx-trigger="click"
27+ hx-delete="/htmx/container-manager/delete/{{.ShortID}}"
28+ hx-swap="outerHTML swap:outerHTML"
29+ hx-target-error="container-delete-images-error"
30+ x-on:htmx:afterSwap="if ($event.detail.status === 200 && $event.detail.xhr.response === 'Success') { success = true; loading = false; setTimeout(() => location.reload(), 3000); }"
31+ >
32+ <span x-show="!loading && !success">Remove</span>
33+ <span x-show="success">Success</span>
34+ </button>
35+ </div>
36+ </div>
37+ </div>
38+ {{ else }}
39+ <div class="mt-2">No Running containers.</div>
40+ {{ end }}
41+ <div
42+ id="container-delete-error"
43+ style="display: none"
44+ class="mt-2 text-red-800 text-lg"
45+ ></div>
46+</div>
@@ -0,0 +1,46 @@
1+<div class="flex flex-col p-4">
2+ <h1 class="text-lg md:text-xl font-bold">Running containers</h1>
3+ <div class="flex font-bold border-t-2 align-baseline py-2">
4+ <div class="w-1/6">Name</div>
5+ <div class="w-1/6">Command</div>
6+ <div class="w-1/6">Created</div>
7+ <div class="w-1/6">Status</div>
8+ <div class="w-1/6">Port</div>
9+ <div class="w-1/6">State</div>
10+ <div class="w-1/4">Action</div>
11+ </div>
12+ {{ range.Containers }}
13+ <div id="{{.ID}}" class="flex py-2 align-baseline justify-center">
14+ <div class="w-1/6">{{ range.Names }}{{.}}<br />{{ end }}</div>
15+ <div class="w-1/6" style="text-decoration: underline">{{.Command}}</div>
16+ <div class="w-1/6">{{.CreatedStr}}</div>
17+ <div class="w-1/6">{{.Status}} (Up since {{.UpSince}})</div>
18+ <div class="w-1/6">{{ range.Ports }}{{.}}<br />{{ end }}</div>
19+ <div class="w-1/6" style="color: {{.StateColor}};">{{.State}}</div>
20+ <div class="w-1/4">
21+ <div x-data="{ success: false, loading: false }">
22+ <button
23+ id="remove-button-{{.ShortID}}"
24+ x-bind:class="{ 'bg-green-500': success, 'bg-red-500': !success, 'text-white': true, 'px-2': true, 'py-1': true, 'rounded': true }"
25+ @click="loading = true"
26+ hx-trigger="click"
27+ hx-delete="/htmx/container-manager/delete/{{.ShortID}}"
28+ hx-swap="outerHTML swap:outerHTML"
29+ hx-target-error="container-delete-images-error"
30+ x-on:htmx:afterSwap="if ($event.detail.status === 200 && $event.detail.xhr.response === 'Success') { success = true; loading = false; setTimeout(() => location.reload(), 3000); }"
31+ >
32+ <span x-show="!loading && !success">Remove</span>
33+ <span x-show="success">Success</span>
34+ </button>
35+ </div>
36+ </div>
37+ </div>
38+ {{ else }}
39+ <div class="mt-2">No Running containers.</div>
40+ {{ end }}
41+ <div
42+ id="container-delete-error"
43+ style="display: none"
44+ class="mt-2 text-red-800 text-lg"
45+ ></div>
46+</div>
D internal/gotemplate/models/html/fragments/image_list.gohtml +0 -34
@@ -1,34 +0,0 @@
1-<div class="flex flex-col p-4">
2- <h1 class="text-lg md:text-xl font-bold">Images</h1>
3- <div class="flex font-bold border-t-2 align-baseline py-2">
4- <div class="w-2/4">Name</div>
5- <div class="w-2/4">Created</div>
6- <div class="w-1/6">Size</div>
7- <div class="w-1/4">Action</div>
8- </div>
9- {{ range.Images }}
10- <div id="{{.ShortID}}" class="flex py-2 align-baseline justify-center">
11- <div class="w-2/4">{{ range.RepoTags }}{{.}}<br />{{ end }}</div>
12- <div class="w-2/4">{{.CreatedStr}}</div>
13- <div class="w-1/6">{{.SizeStr}}</div>
14- <div class="w-1/4">
15- <button
16- hx-trigger="click"
17- hx-delete="/htmx/image-manager/delete/{{.ShortID}}"
18- hx-swap="innerHTML"
19- hx-target-error="container-delete-images-error"
20- class="bg-red-500 text-white px-2 py-1 rounded"
21- >
22- Remove
23- </button>
24- </div>
25- </div>
26- {{ else }}
27- <div class="mt-2">No images found.</div>
28- {{ end }}
29- <div
30- id="container-delete-images-error"
31- style="display: none"
32- class="mt-2 text-red-800 text-lg"
33- ></div>
34-</div>
@@ -1,34 +0,0 @@
1-<div class="flex flex-col p-4">
2- <h1 class="text-lg md:text-xl font-bold">Images</h1>
3- <div class="flex font-bold border-t-2 align-baseline py-2">
4- <div class="w-2/4">Name</div>
5- <div class="w-2/4">Created</div>
6- <div class="w-1/6">Size</div>
7- <div class="w-1/4">Action</div>
8- </div>
9- {{ range.Images }}
10- <div id="{{.ShortID}}" class="flex py-2 align-baseline justify-center">
11- <div class="w-2/4">{{ range.RepoTags }}{{.}}<br />{{ end }}</div>
12- <div class="w-2/4">{{.CreatedStr}}</div>
13- <div class="w-1/6">{{.SizeStr}}</div>
14- <div class="w-1/4">
15- <button
16- hx-trigger="click"
17- hx-delete="/htmx/image-manager/delete/{{.ShortID}}"
18- hx-swap="innerHTML"
19- hx-target-error="container-delete-images-error"
20- class="bg-red-500 text-white px-2 py-1 rounded"
21- >
22- Remove
23- </button>
24- </div>
25- </div>
26- {{ else }}
27- <div class="mt-2">No images found.</div>
28- {{ end }}
29- <div
30- id="container-delete-images-error"
31- style="display: none"
32- class="mt-2 text-red-800 text-lg"
33- ></div>
34-</div>
A internal/gotemplate/models/html/fragments/imagelist.gohtml +56 -0
@@ -0,0 +1,56 @@
1+<div class="flex flex-col p-4">
2+ <h1 class="text-lg md:text-xl font-bold">Images</h1>
3+ <div class="flex font-bold border-t-2 align-baseline py-2">
4+ <div class="w-2/4">Name</div>
5+ <div class="w-2/4">Created</div>
6+ <div class="w-1/6">Size</div>
7+ <div class="w-1/4">Action</div>
8+ </div>
9+ {{ range.Images }}
10+ <div
11+ x-data="{ success: false, loading: false, showError: false, errorMessage: '' }"
12+ >
13+ <div
14+ id="img-{{.ShortImgID}}"
15+ class="flex py-2 align-baseline justify-center"
16+ hx-boost="true"
17+ >
18+ <div class="w-2/4">
19+ <span title="{{.ImageSummary.ID}}">{{.Name}}</span>
20+ </div>
21+ <div class="w-2/4">{{.CreatedStr}}</div>
22+ <div class="w-1/6">{{.SizeStr}}</div>
23+
24+ <button
25+ id="remove-button-img-{{.ShortImgID}}"
26+ x-bind:class="{ 'bg-green-500': success, 'bg-red-500': !success, 'text-white': true, 'px-2': true, 'py-1': true, 'rounded': true }"
27+ @click="loading = true"
28+ hx-trigger="click"
29+ hx-delete="/htmx/image-manager/delete/{{.ShortImgID}}"
30+ hx-swap="outerHTML swap:outerHTML"
31+ hx-target="#img-{{.ShortImgID}}"
32+ hx-confirm="Are you sure you want to delete this image?"
33+ hx-select="button"
34+ hx-boost="true"
35+ x-on:htmx:after-swap="success = true; loading = false; error = false"
36+ x-on:htmx:responseError="showError = true; errorMessage = (event.detail && event.detail.xhr && event.detail.xhr.responseText) ? event.detail.xhr.responseText : 'An unknown error occurred.'"
37+ >
38+ <span x-show="!loading && !success">Remove</span>
39+ <span x-show="loading">Loading...</span>
40+ <span x-show="success">Success</span>
41+ </button>
42+ </div>
43+
44+ <div
45+ x-show="showError"
46+ data-error-type="image"
47+ class="error-display-div bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"
48+ >
49+ <p x-text="errorMessage"></p>
50+ </div>
51+ </div>
52+
53+ {{ else }}
54+ <div class="mt-2">No images found.</div>
55+ {{ end }}
56+</div>
@@ -0,0 +1,56 @@
1+<div class="flex flex-col p-4">
2+ <h1 class="text-lg md:text-xl font-bold">Images</h1>
3+ <div class="flex font-bold border-t-2 align-baseline py-2">
4+ <div class="w-2/4">Name</div>
5+ <div class="w-2/4">Created</div>
6+ <div class="w-1/6">Size</div>
7+ <div class="w-1/4">Action</div>
8+ </div>
9+ {{ range.Images }}
10+ <div
11+ x-data="{ success: false, loading: false, showError: false, errorMessage: '' }"
12+ >
13+ <div
14+ id="img-{{.ShortImgID}}"
15+ class="flex py-2 align-baseline justify-center"
16+ hx-boost="true"
17+ >
18+ <div class="w-2/4">
19+ <span title="{{.ImageSummary.ID}}">{{.Name}}</span>
20+ </div>
21+ <div class="w-2/4">{{.CreatedStr}}</div>
22+ <div class="w-1/6">{{.SizeStr}}</div>
23+
24+ <button
25+ id="remove-button-img-{{.ShortImgID}}"
26+ x-bind:class="{ 'bg-green-500': success, 'bg-red-500': !success, 'text-white': true, 'px-2': true, 'py-1': true, 'rounded': true }"
27+ @click="loading = true"
28+ hx-trigger="click"
29+ hx-delete="/htmx/image-manager/delete/{{.ShortImgID}}"
30+ hx-swap="outerHTML swap:outerHTML"
31+ hx-target="#img-{{.ShortImgID}}"
32+ hx-confirm="Are you sure you want to delete this image?"
33+ hx-select="button"
34+ hx-boost="true"
35+ x-on:htmx:after-swap="success = true; loading = false; error = false"
36+ x-on:htmx:responseError="showError = true; errorMessage = (event.detail && event.detail.xhr && event.detail.xhr.responseText) ? event.detail.xhr.responseText : 'An unknown error occurred.'"
37+ >
38+ <span x-show="!loading && !success">Remove</span>
39+ <span x-show="loading">Loading...</span>
40+ <span x-show="success">Success</span>
41+ </button>
42+ </div>
43+
44+ <div
45+ x-show="showError"
46+ data-error-type="image"
47+ class="error-display-div bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"
48+ >
49+ <p x-text="errorMessage"></p>
50+ </div>
51+ </div>
52+
53+ {{ else }}
54+ <div class="mt-2">No images found.</div>
55+ {{ end }}
56+</div>
M internal/httpserve/handler/adminroutes.go +0 -16
@@ -8,8 +8,6 @@ import (
88 "github.com/bnema/gordon/internal/gotemplate/render"
99 "github.com/bnema/gordon/internal/httpserve/middleware"
1010 "github.com/bnema/gordon/internal/webui"
11- "github.com/bnema/gordon/pkg/utils/docker"
12- "github.com/bnema/gordon/pkg/utils/sanitize"
1311 "github.com/labstack/echo/v4"
1412 )
1513
@@ -61,22 +59,8 @@ func AdminManagerRoute(c echo.Context, a *app.App) error {
6159 if err != nil {
6260 return err
6361 }
64-
65- images, err := docker.ListContainerImages()
66- if err != nil {
67- rawErrHTML := `<div id="container-images">Error: ` + err.Error() + `</div>`
68- sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
69- if err != nil {
70- // Handle the sanitization error
71- return c.String(http.StatusInternalServerError, "An error occurred during sanitization")
72- }
73- return c.HTML(http.StatusInternalServerError, sanitizedHTML)
74- }
75-
76- // Create a data map to pass to the renderer
7762 data := map[string]interface{}{
7863 "CurrentLang": yamlData.CurrentLang,
79- "Images": images,
8064 }
8165
8266 renderedHTML, err := rendererData.Render(data, a)
@@ -8,8 +8,6 @@ import (
8 "github.com/bnema/gordon/internal/gotemplate/render"8 "github.com/bnema/gordon/internal/gotemplate/render"
9 "github.com/bnema/gordon/internal/httpserve/middleware"9 "github.com/bnema/gordon/internal/httpserve/middleware"
10 "github.com/bnema/gordon/internal/webui"10 "github.com/bnema/gordon/internal/webui"
11- "github.com/bnema/gordon/pkg/utils/docker"
12- "github.com/bnema/gordon/pkg/utils/sanitize"
13 "github.com/labstack/echo/v4"11 "github.com/labstack/echo/v4"
14 )12 )
15 13
@@ -61,22 +59,8 @@ func AdminManagerRoute(c echo.Context, a *app.App) error {
61 if err != nil {59 if err != nil {
62 return err60 return err
63 }61 }
64-
65- images, err := docker.ListContainerImages()
66- if err != nil {
67- rawErrHTML := `<div id="container-images">Error: ` + err.Error() + `</div>`
68- sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
69- if err != nil {
70- // Handle the sanitization error
71- return c.String(http.StatusInternalServerError, "An error occurred during sanitization")
72- }
73- return c.HTML(http.StatusInternalServerError, sanitizedHTML)
74- }
75-
76- // Create a data map to pass to the renderer
77 data := map[string]interface{}{62 data := map[string]interface{}{
78 "CurrentLang": yamlData.CurrentLang,63 "CurrentLang": yamlData.CurrentLang,
79- "Images": images,
80 }64 }
81 65
82 renderedHTML, err := rendererData.Render(data, a)66 renderedHTML, err := rendererData.Render(data, a)
M internal/httpserve/handler/docker.go +75 -15
@@ -18,11 +18,20 @@ var idMap = make(map[string]string)
1818
1919 type HumanReadableContainerImage struct {
2020 *types.ImageSummary
21- ShortID string
21+ Name string
22+ ShortImgID string
2223 CreatedStr string
2324 SizeStr string
2425 }
2526
27+type HumanReadableContainer struct {
28+ *types.Container
29+ CreatedStr string // Human-readable Created time
30+ SizeStr string // Human-readable Size
31+ UpSince string // Human-readable time elapsed since the container was started
32+ StateColor string // Color of the state badge
33+}
34+
2635 // ImageManagerHandler handles the /image-manager route
2736 func ImageManagerHandler(c echo.Context, a *app.App) error {
2837 images, err := docker.ListContainerImages()
@@ -38,24 +47,26 @@ func ImageManagerHandler(c echo.Context, a *app.App) error {
3847 var humanReadableImages []HumanReadableContainerImage
3948
4049 for _, image := range images {
41- ShortID := image.ID[7:19]
42- idMap[ShortID] = image.ID
50+ ShortImgID := image.ID[7:19]
51+ idMap[ShortImgID] = image.ID
4352 createdTime := time.Unix(image.Created, 0)
4453 createdStr := humanize.TimeAgo(createdTime)
4554 sizeStr := humanize.BytesToReadableSize(image.Size)
46-
47- humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{
48- ShortID: ShortID,
49- ImageSummary: &image,
50- CreatedStr: createdStr,
51- SizeStr: sizeStr,
52- })
55+ for _, repoTag := range image.RepoTags {
56+ humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{
57+ ShortImgID: ShortImgID,
58+ ImageSummary: &image,
59+ CreatedStr: createdStr,
60+ SizeStr: sizeStr,
61+ Name: repoTag,
62+ })
63+ }
5364 }
5465 data := map[string]interface{}{
5566 "Images": humanReadableImages,
5667 }
5768
58- rendererData, err := render.GetHTMLRenderer("html/fragments", "image_list.gohtml", a.TemplateFS, a)
69+ rendererData, err := render.GetHTMLRenderer("html/fragments", "imagelist.gohtml", a.TemplateFS, a)
5970 if err != nil {
6071 return err
6172 }
@@ -68,16 +79,65 @@ func ImageManagerHandler(c echo.Context, a *app.App) error {
6879
6980 // ImageManagerDeleteHandler handles the /image-manager/delete route
7081 func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {
82+ // Get the ShortImgID from the URL
83+ ShortImgID := c.Param("ShortImgID")
84+ fmt.Println("ShortImgID:", ShortImgID)
85+ // Check if the ShortImgID exists in the idMap
86+ imageID, exists := idMap[ShortImgID]
87+ fmt.Println("ImageID:", imageID)
88+ if !exists {
89+ return c.String(http.StatusBadRequest, "Invalid ShortImgID")
90+ }
91+ err := docker.DeleteContainerImage(imageID)
92+ if err != nil {
93+ c.Response().Header().Set("X-Error-Type", "image")
94+ return c.String(http.StatusInternalServerError, err.Error())
95+
96+ }
97+
98+ // Since it is HTMX we return a html div with the message "Removed"
99+ return c.HTML(http.StatusOK, `<div>Removed</div>`)
100+}
101+
102+// ContainerManagerHandler handles the /container-manager route
103+func ContainerManagerHandler(c echo.Context, a *app.App) error {
104+ containers, err := docker.ListRunningContainers()
105+ if err != nil {
106+ rawErrHTML := `<div>Error: ` + err.Error() + `</div>`
107+ sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
108+ if err != nil {
109+ return c.String(http.StatusInternalServerError, "An error occurred during sanitization")
110+ }
111+ return c.HTML(http.StatusInternalServerError, sanitizedHTML)
112+ }
113+
114+ data := map[string]interface{}{
115+ "Containers": containers,
116+ }
117+
118+ rendererData, err := render.GetHTMLRenderer("html/fragments", "containerlist.gohtml", a.TemplateFS, a)
119+ if err != nil {
120+ return err
121+ }
122+ renderedHTML, err := rendererData.Render(data, a)
123+ if err != nil {
124+ return err
125+ }
126+ return c.HTML(200, renderedHTML)
127+}
128+
129+// ContainerManagerDeleteHandler handles the /container-manager/delete route
130+func ContainerManagerDeleteHandler(c echo.Context, a *app.App) error {
71131 // Get the ShortID from the URL
72132 ShortID := c.Param("ShortID")
73133 // Check if the shortID exists in the idMap
74- imageID, exists := idMap[ShortID]
134+ containerID, exists := idMap[ShortID]
75135 if !exists {
76136 return c.String(http.StatusBadRequest, "Invalid shortID")
77137 }
78- fmt.Println("ShortID:", ShortID, "ImageID:", imageID)
138+ fmt.Println("ShortID:", ShortID, "ContainerID:", containerID)
79139
80- err := docker.DeleteContainerImage(imageID)
140+ err := docker.DeleteContainer(containerID)
81141 if err != nil {
82142 rawErrHTML := `<div>Error: ` + err.Error() + `</div>`
83143 sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
@@ -88,5 +148,5 @@ func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {
88148 return c.HTML(http.StatusInternalServerError, sanitizedHTML)
89149 }
90150 // Since it is HTMX we return a html div with the message "Removed"
91- return c.HTML(http.StatusOK, `<div>Removed</div>`)
151+ return c.HTML(http.StatusOK, `Success`)
92152 }
@@ -18,11 +18,20 @@ var idMap = make(map[string]string)
18 18
19 type HumanReadableContainerImage struct {19 type HumanReadableContainerImage struct {
20 *types.ImageSummary20 *types.ImageSummary
21- ShortID string21+ Name string
22+ ShortImgID string
22 CreatedStr string23 CreatedStr string
23 SizeStr string24 SizeStr string
24 }25 }
25 26
27+type HumanReadableContainer struct {
28+ *types.Container
29+ CreatedStr string // Human-readable Created time
30+ SizeStr string // Human-readable Size
31+ UpSince string // Human-readable time elapsed since the container was started
32+ StateColor string // Color of the state badge
33+}
34+
26 // ImageManagerHandler handles the /image-manager route35 // ImageManagerHandler handles the /image-manager route
27 func ImageManagerHandler(c echo.Context, a *app.App) error {36 func ImageManagerHandler(c echo.Context, a *app.App) error {
28 images, err := docker.ListContainerImages()37 images, err := docker.ListContainerImages()
@@ -38,24 +47,26 @@ func ImageManagerHandler(c echo.Context, a *app.App) error {
38 var humanReadableImages []HumanReadableContainerImage47 var humanReadableImages []HumanReadableContainerImage
39 48
40 for _, image := range images {49 for _, image := range images {
41- ShortID := image.ID[7:19]50+ ShortImgID := image.ID[7:19]
42- idMap[ShortID] = image.ID51+ idMap[ShortImgID] = image.ID
43 createdTime := time.Unix(image.Created, 0)52 createdTime := time.Unix(image.Created, 0)
44 createdStr := humanize.TimeAgo(createdTime)53 createdStr := humanize.TimeAgo(createdTime)
45 sizeStr := humanize.BytesToReadableSize(image.Size)54 sizeStr := humanize.BytesToReadableSize(image.Size)
46-55+ for _, repoTag := range image.RepoTags {
47- humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{56+ humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{
48- ShortID: ShortID,57+ ShortImgID: ShortImgID,
49- ImageSummary: &image,58+ ImageSummary: &image,
50- CreatedStr: createdStr,59+ CreatedStr: createdStr,
51- SizeStr: sizeStr,60+ SizeStr: sizeStr,
52- })61+ Name: repoTag,
62+ })
63+ }
53 }64 }
54 data := map[string]interface{}{65 data := map[string]interface{}{
55 "Images": humanReadableImages,66 "Images": humanReadableImages,
56 }67 }
57 68
58- rendererData, err := render.GetHTMLRenderer("html/fragments", "image_list.gohtml", a.TemplateFS, a)69+ rendererData, err := render.GetHTMLRenderer("html/fragments", "imagelist.gohtml", a.TemplateFS, a)
59 if err != nil {70 if err != nil {
60 return err71 return err
61 }72 }
@@ -68,16 +79,65 @@ func ImageManagerHandler(c echo.Context, a *app.App) error {
68 79
69 // ImageManagerDeleteHandler handles the /image-manager/delete route80 // ImageManagerDeleteHandler handles the /image-manager/delete route
70 func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {81 func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {
82+ // Get the ShortImgID from the URL
83+ ShortImgID := c.Param("ShortImgID")
84+ fmt.Println("ShortImgID:", ShortImgID)
85+ // Check if the ShortImgID exists in the idMap
86+ imageID, exists := idMap[ShortImgID]
87+ fmt.Println("ImageID:", imageID)
88+ if !exists {
89+ return c.String(http.StatusBadRequest, "Invalid ShortImgID")
90+ }
91+ err := docker.DeleteContainerImage(imageID)
92+ if err != nil {
93+ c.Response().Header().Set("X-Error-Type", "image")
94+ return c.String(http.StatusInternalServerError, err.Error())
95+
96+ }
97+
98+ // Since it is HTMX we return a html div with the message "Removed"
99+ return c.HTML(http.StatusOK, `<div>Removed</div>`)
100+}
101+
102+// ContainerManagerHandler handles the /container-manager route
103+func ContainerManagerHandler(c echo.Context, a *app.App) error {
104+ containers, err := docker.ListRunningContainers()
105+ if err != nil {
106+ rawErrHTML := `<div>Error: ` + err.Error() + `</div>`
107+ sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
108+ if err != nil {
109+ return c.String(http.StatusInternalServerError, "An error occurred during sanitization")
110+ }
111+ return c.HTML(http.StatusInternalServerError, sanitizedHTML)
112+ }
113+
114+ data := map[string]interface{}{
115+ "Containers": containers,
116+ }
117+
118+ rendererData, err := render.GetHTMLRenderer("html/fragments", "containerlist.gohtml", a.TemplateFS, a)
119+ if err != nil {
120+ return err
121+ }
122+ renderedHTML, err := rendererData.Render(data, a)
123+ if err != nil {
124+ return err
125+ }
126+ return c.HTML(200, renderedHTML)
127+}
128+
129+// ContainerManagerDeleteHandler handles the /container-manager/delete route
130+func ContainerManagerDeleteHandler(c echo.Context, a *app.App) error {
71 // Get the ShortID from the URL131 // Get the ShortID from the URL
72 ShortID := c.Param("ShortID")132 ShortID := c.Param("ShortID")
73 // Check if the shortID exists in the idMap133 // Check if the shortID exists in the idMap
74- imageID, exists := idMap[ShortID]134+ containerID, exists := idMap[ShortID]
75 if !exists {135 if !exists {
76 return c.String(http.StatusBadRequest, "Invalid shortID")136 return c.String(http.StatusBadRequest, "Invalid shortID")
77 }137 }
78- fmt.Println("ShortID:", ShortID, "ImageID:", imageID)138+ fmt.Println("ShortID:", ShortID, "ContainerID:", containerID)
79 139
80- err := docker.DeleteContainerImage(imageID)140+ err := docker.DeleteContainer(containerID)
81 if err != nil {141 if err != nil {
82 rawErrHTML := `<div>Error: ` + err.Error() + `</div>`142 rawErrHTML := `<div>Error: ` + err.Error() + `</div>`
83 sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)143 sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
@@ -88,5 +148,5 @@ func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {
88 return c.HTML(http.StatusInternalServerError, sanitizedHTML)148 return c.HTML(http.StatusInternalServerError, sanitizedHTML)
89 }149 }
90 // Since it is HTMX we return a html div with the message "Removed"150 // Since it is HTMX we return a html div with the message "Removed"
91- return c.HTML(http.StatusOK, `<div>Removed</div>`)151+ return c.HTML(http.StatusOK, `Success`)
92 }152 }
M internal/httpserve/router.go +8 -1
@@ -70,7 +70,14 @@ func bindHTMXEndpoints(e *echo.Echo, a *app.App) {
7070 e.GET("/htmx/image-manager", func(c echo.Context) error {
7171 return handler.ImageManagerHandler(c, a)
7272 })
73- e.DELETE("/htmx/image-manager/delete/:ShortID", func(c echo.Context) error {
73+ e.DELETE("/htmx/image-manager/delete/:ShortImgID", func(c echo.Context) error {
7474 return handler.ImageManagerDeleteHandler(c, a)
7575 })
76+
77+ e.GET("/htmx/container-manager", func(c echo.Context) error {
78+ return handler.ContainerManagerHandler(c, a)
79+ })
80+ e.DELETE("/htmx/container-manager/delete/:ShortID", func(c echo.Context) error {
81+ return handler.ContainerManagerDeleteHandler(c, a)
82+ })
7683 }
@@ -70,7 +70,14 @@ func bindHTMXEndpoints(e *echo.Echo, a *app.App) {
70 e.GET("/htmx/image-manager", func(c echo.Context) error {70 e.GET("/htmx/image-manager", func(c echo.Context) error {
71 return handler.ImageManagerHandler(c, a)71 return handler.ImageManagerHandler(c, a)
72 })72 })
73- e.DELETE("/htmx/image-manager/delete/:ShortID", func(c echo.Context) error {73+ e.DELETE("/htmx/image-manager/delete/:ShortImgID", func(c echo.Context) error {
74 return handler.ImageManagerDeleteHandler(c, a)74 return handler.ImageManagerDeleteHandler(c, a)
75 })75 })
76+
77+ e.GET("/htmx/container-manager", func(c echo.Context) error {
78+ return handler.ContainerManagerHandler(c, a)
79+ })
80+ e.DELETE("/htmx/container-manager/delete/:ShortID", func(c echo.Context) error {
81+ return handler.ContainerManagerDeleteHandler(c, a)
82+ })
76 }83 }
M internal/webui/public/assets/css/tailwind.css +77 -5
@@ -541,6 +541,10 @@ video {
541541 --tw-backdrop-sepia: ;
542542 }
543543
544+.relative {
545+ position: relative;
546+}
547+
544548 .my-2 {
545549 margin-top: 0.5rem;
546550 margin-bottom: 0.5rem;
@@ -639,6 +643,22 @@ video {
639643 flex-grow: 1;
640644 }
641645
646+.scale-100 {
647+ --tw-scale-x: 1;
648+ --tw-scale-y: 1;
649+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
650+}
651+
652+.scale-90 {
653+ --tw-scale-x: .9;
654+ --tw-scale-y: .9;
655+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
656+}
657+
658+.transform {
659+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
660+}
661+
642662 .flex-row {
643663 flex-direction: row;
644664 }
@@ -703,11 +723,6 @@ video {
703723 border-width: 1px;
704724 }
705725
706-.border-y-2 {
707- border-top-width: 2px;
708- border-bottom-width: 2px;
709-}
710-
711726 .border-t-2 {
712727 border-top-width: 2px;
713728 }
@@ -717,6 +732,11 @@ video {
717732 border-color: rgb(209 213 219 / var(--tw-border-opacity));
718733 }
719734
735+.border-red-400 {
736+ --tw-border-opacity: 1;
737+ border-color: rgb(248 113 113 / var(--tw-border-opacity));
738+}
739+
720740 .bg-gordonmascot-hblue {
721741 --tw-bg-opacity: 1;
722742 background-color: rgb(1 30 92 / var(--tw-bg-opacity));
@@ -732,6 +752,16 @@ video {
732752 background-color: rgb(209 213 219 / var(--tw-bg-opacity));
733753 }
734754
755+.bg-green-500 {
756+ --tw-bg-opacity: 1;
757+ background-color: rgb(34 197 94 / var(--tw-bg-opacity));
758+}
759+
760+.bg-red-100 {
761+ --tw-bg-opacity: 1;
762+ background-color: rgb(254 226 226 / var(--tw-bg-opacity));
763+}
764+
735765 .bg-red-500 {
736766 --tw-bg-opacity: 1;
737767 background-color: rgb(239 68 68 / var(--tw-bg-opacity));
@@ -783,6 +813,11 @@ video {
783813 padding-bottom: 0.5rem;
784814 }
785815
816+.py-3 {
817+ padding-top: 0.75rem;
818+ padding-bottom: 0.75rem;
819+}
820+
786821 .align-baseline {
787822 vertical-align: baseline;
788823 }
@@ -844,6 +879,11 @@ video {
844879 color: rgb(31 41 55 / var(--tw-text-opacity));
845880 }
846881
882+.text-red-700 {
883+ --tw-text-opacity: 1;
884+ color: rgb(185 28 28 / var(--tw-text-opacity));
885+}
886+
847887 .text-red-800 {
848888 --tw-text-opacity: 1;
849889 color: rgb(153 27 27 / var(--tw-text-opacity));
@@ -859,6 +899,18 @@ video {
859899 color: rgb(255 255 255 / var(--tw-text-opacity));
860900 }
861901
902+.underline {
903+ text-decoration-line: underline;
904+}
905+
906+.opacity-0 {
907+ opacity: 0;
908+}
909+
910+.opacity-100 {
911+ opacity: 1;
912+}
913+
862914 .shadow {
863915 --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
864916 --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
@@ -870,6 +922,26 @@ video {
870922 outline-offset: 2px;
871923 }
872924
925+.transition {
926+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
927+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
928+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
929+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
930+ transition-duration: 150ms;
931+}
932+
933+.duration-300 {
934+ transition-duration: 300ms;
935+}
936+
937+.ease-in {
938+ transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
939+}
940+
941+.ease-out {
942+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
943+}
944+
873945 /* Import IBM Plex Fonts */
874946
875947 @font-face {
@@ -541,6 +541,10 @@ video {
541 --tw-backdrop-sepia: ;541 --tw-backdrop-sepia: ;
542 }542 }
543 543
544+.relative {
545+ position: relative;
546+}
547+
544 .my-2 {548 .my-2 {
545 margin-top: 0.5rem;549 margin-top: 0.5rem;
546 margin-bottom: 0.5rem;550 margin-bottom: 0.5rem;
@@ -639,6 +643,22 @@ video {
639 flex-grow: 1;643 flex-grow: 1;
640 }644 }
641 645
646+.scale-100 {
647+ --tw-scale-x: 1;
648+ --tw-scale-y: 1;
649+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
650+}
651+
652+.scale-90 {
653+ --tw-scale-x: .9;
654+ --tw-scale-y: .9;
655+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
656+}
657+
658+.transform {
659+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
660+}
661+
642 .flex-row {662 .flex-row {
643 flex-direction: row;663 flex-direction: row;
644 }664 }
@@ -703,11 +723,6 @@ video {
703 border-width: 1px;723 border-width: 1px;
704 }724 }
705 725
706-.border-y-2 {
707- border-top-width: 2px;
708- border-bottom-width: 2px;
709-}
710-
711 .border-t-2 {726 .border-t-2 {
712 border-top-width: 2px;727 border-top-width: 2px;
713 }728 }
@@ -717,6 +732,11 @@ video {
717 border-color: rgb(209 213 219 / var(--tw-border-opacity));732 border-color: rgb(209 213 219 / var(--tw-border-opacity));
718 }733 }
719 734
735+.border-red-400 {
736+ --tw-border-opacity: 1;
737+ border-color: rgb(248 113 113 / var(--tw-border-opacity));
738+}
739+
720 .bg-gordonmascot-hblue {740 .bg-gordonmascot-hblue {
721 --tw-bg-opacity: 1;741 --tw-bg-opacity: 1;
722 background-color: rgb(1 30 92 / var(--tw-bg-opacity));742 background-color: rgb(1 30 92 / var(--tw-bg-opacity));
@@ -732,6 +752,16 @@ video {
732 background-color: rgb(209 213 219 / var(--tw-bg-opacity));752 background-color: rgb(209 213 219 / var(--tw-bg-opacity));
733 }753 }
734 754
755+.bg-green-500 {
756+ --tw-bg-opacity: 1;
757+ background-color: rgb(34 197 94 / var(--tw-bg-opacity));
758+}
759+
760+.bg-red-100 {
761+ --tw-bg-opacity: 1;
762+ background-color: rgb(254 226 226 / var(--tw-bg-opacity));
763+}
764+
735 .bg-red-500 {765 .bg-red-500 {
736 --tw-bg-opacity: 1;766 --tw-bg-opacity: 1;
737 background-color: rgb(239 68 68 / var(--tw-bg-opacity));767 background-color: rgb(239 68 68 / var(--tw-bg-opacity));
@@ -783,6 +813,11 @@ video {
783 padding-bottom: 0.5rem;813 padding-bottom: 0.5rem;
784 }814 }
785 815
816+.py-3 {
817+ padding-top: 0.75rem;
818+ padding-bottom: 0.75rem;
819+}
820+
786 .align-baseline {821 .align-baseline {
787 vertical-align: baseline;822 vertical-align: baseline;
788 }823 }
@@ -844,6 +879,11 @@ video {
844 color: rgb(31 41 55 / var(--tw-text-opacity));879 color: rgb(31 41 55 / var(--tw-text-opacity));
845 }880 }
846 881
882+.text-red-700 {
883+ --tw-text-opacity: 1;
884+ color: rgb(185 28 28 / var(--tw-text-opacity));
885+}
886+
847 .text-red-800 {887 .text-red-800 {
848 --tw-text-opacity: 1;888 --tw-text-opacity: 1;
849 color: rgb(153 27 27 / var(--tw-text-opacity));889 color: rgb(153 27 27 / var(--tw-text-opacity));
@@ -859,6 +899,18 @@ video {
859 color: rgb(255 255 255 / var(--tw-text-opacity));899 color: rgb(255 255 255 / var(--tw-text-opacity));
860 }900 }
861 901
902+.underline {
903+ text-decoration-line: underline;
904+}
905+
906+.opacity-0 {
907+ opacity: 0;
908+}
909+
910+.opacity-100 {
911+ opacity: 1;
912+}
913+
862 .shadow {914 .shadow {
863 --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);915 --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
864 --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);916 --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
@@ -870,6 +922,26 @@ video {
870 outline-offset: 2px;922 outline-offset: 2px;
871 }923 }
872 924
925+.transition {
926+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
927+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
928+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
929+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
930+ transition-duration: 150ms;
931+}
932+
933+.duration-300 {
934+ transition-duration: 300ms;
935+}
936+
937+.ease-in {
938+ transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
939+}
940+
941+.ease-out {
942+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
943+}
944+
873 /* Import IBM Plex Fonts */945 /* Import IBM Plex Fonts */
874 946
875 @font-face {947 @font-face {
M internal/webui/public/assets/js/custom.js +14 -14
@@ -1,15 +1,15 @@
1-document.body.addEventListener('htmx:afterRequest', function (evt) {
2- const targetError = evt.target.attributes.getNamedItem('hx-target-error');
3- if (evt.detail.failed && targetError) {
4- const errorElement = document.getElementById(targetError.value);
5- errorElement.innerHTML = evt.detail.xhr.responseText; // Set the response text as content
6- errorElement.style.display = "inline";
7- }
8-});
9-
10-document.body.addEventListener('htmx:beforeRequest', function (evt) {
11- const targetError = evt.target.attributes.getNamedItem('hx-target-error');
12- if (targetError) {
13- document.getElementById(targetError.value).style.display = "none";
14- }
1+document.addEventListener('DOMContentLoaded', function() {
2+ document.body.addEventListener('htmx:responseError', function(event) {
3+ const errorDetail = event.detail;
4+ const errorType = errorDetail && errorDetail.xhr ? errorDetail.xhr.getResponseHeader("X-Error-Type") : 'generic'; // Fallback to 'generic' if not set
5+ const errorDiv = document.querySelector(`div[data-error-type="${errorType}"]`);
6+ if (errorDiv) {
7+ if (errorDetail && errorDetail.xhr && errorDetail.xhr.responseText) {
8+ errorDiv.innerHTML = errorDetail.xhr.responseText;
9+ } else {
10+ errorDiv.textContent = 'An unknown error occurred.';
11+ }
12+ errorDiv.style.display = 'block'; // Make it visible
13+ }
14+ });
1515 });
@@ -1,15 +1,15 @@
1-document.body.addEventListener('htmx:afterRequest', function (evt) {1+document.addEventListener('DOMContentLoaded', function() {
2- const targetError = evt.target.attributes.getNamedItem('hx-target-error');2+ document.body.addEventListener('htmx:responseError', function(event) {
3- if (evt.detail.failed && targetError) {3+ const errorDetail = event.detail;
4- const errorElement = document.getElementById(targetError.value);4+ const errorType = errorDetail && errorDetail.xhr ? errorDetail.xhr.getResponseHeader("X-Error-Type") : 'generic'; // Fallback to 'generic' if not set
5- errorElement.innerHTML = evt.detail.xhr.responseText; // Set the response text as content5+ const errorDiv = document.querySelector(`div[data-error-type="${errorType}"]`);
6- errorElement.style.display = "inline";6+ if (errorDiv) {
7- }7+ if (errorDetail && errorDetail.xhr && errorDetail.xhr.responseText) {
8-});8+ errorDiv.innerHTML = errorDetail.xhr.responseText;
9-9+ } else {
10-document.body.addEventListener('htmx:beforeRequest', function (evt) {10+ errorDiv.textContent = 'An unknown error occurred.';
11- const targetError = evt.target.attributes.getNamedItem('hx-target-error');11+ }
12- if (targetError) {12+ errorDiv.style.display = 'block'; // Make it visible
13- document.getElementById(targetError.value).style.display = "none";13+ }
14- }14+ });
15 });15 });
M pkg/utils/docker/client.go +8 -0
@@ -37,3 +37,11 @@ func (d *DockerClient) InitializeClient(config *Config) error {
3737 dockerCli = cli
3838 return nil
3939 }
40+
41+func CheckIfInitialized() error {
42+ if dockerCli == nil {
43+ return fmt.Errorf("Docker client is not initialized")
44+ }
45+
46+ return nil
47+}
@@ -37,3 +37,11 @@ func (d *DockerClient) InitializeClient(config *Config) error {
37 dockerCli = cli37 dockerCli = cli
38 return nil38 return nil
39 }39 }
40+
41+func CheckIfInitialized() error {
42+ if dockerCli == nil {
43+ return fmt.Errorf("Docker client is not initialized")
44+ }
45+
46+ return nil
47+}
M pkg/utils/docker/containers.go +61 -5
@@ -2,17 +2,14 @@ package docker
22
33 import (
44 "context"
5- "fmt"
5+ "time"
66
77 "github.com/docker/docker/api/types"
88 )
99
1010 func ListRunningContainers() ([]types.Container, error) {
1111 // Check if the Docker client has been initialized
12- if dockerCli == nil {
13- return nil, fmt.Errorf("Docker client has not been initialized")
14- }
15-
12+ CheckIfInitialized()
1613 // List containers using the Docker client
1714 containers, err := dockerCli.ContainerList(context.Background(), types.ContainerListOptions{})
1815 if err != nil {
@@ -21,3 +18,62 @@ func ListRunningContainers() ([]types.Container, error) {
2118
2219 return containers, nil
2320 }
21+
22+// StopContainer attempts to stop a container gracefully with a timeout
23+// If it doesn't stop, it sets a flag that can be checked to prompt the user.
24+func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) {
25+ // Check if the Docker client has been initialized
26+ CheckIfInitialized()
27+
28+ // Start by sending a SIGTERM
29+ err := dockerCli.ContainerKill(context.Background(), containerID, "SIGTERM")
30+ if err != nil {
31+ return false, err
32+ }
33+
34+ // Initialize a ticker for timeout
35+ ticker := time.NewTicker(1 * time.Second)
36+ defer ticker.Stop()
37+
38+ for elapsed := 0; elapsed < int(timeoutDuration.Seconds()); elapsed++ {
39+ select {
40+ case <-ticker.C:
41+ // Check if the container is still running
42+ container, err := dockerCli.ContainerInspect(context.Background(), containerID)
43+ if err != nil {
44+ return false, err
45+ }
46+
47+ // If the container is not running, return true indicating it was stopped
48+ if !container.State.Running {
49+ return true, nil
50+ }
51+ }
52+ }
53+
54+ // Return false, signaling that the container needs to be force-stopped
55+ return false, nil
56+}
57+
58+func StopContainerRagefully(containerID string) error {
59+ // Start by sending a SIGKILL
60+ err := dockerCli.ContainerKill(context.Background(), containerID, "SIGKILL")
61+ if err != nil {
62+ return err
63+ }
64+
65+ return nil
66+}
67+
68+func DeleteContainer(containerID string) error {
69+ // Check if the Docker client has been initialized
70+ CheckIfInitialized()
71+
72+ // Delete container using the Docker client
73+ err := dockerCli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{})
74+ if err != nil {
75+ return err
76+ }
77+
78+ return nil
79+}
@@ -2,17 +2,14 @@ package docker
2 2
3 import (3 import (
4 "context"4 "context"
5- "fmt"5+ "time"
6 6
7 "github.com/docker/docker/api/types"7 "github.com/docker/docker/api/types"
8 )8 )
9 9
10 func ListRunningContainers() ([]types.Container, error) {10 func ListRunningContainers() ([]types.Container, error) {
11 // Check if the Docker client has been initialized11 // Check if the Docker client has been initialized
12- if dockerCli == nil {12+ CheckIfInitialized()
13- return nil, fmt.Errorf("Docker client has not been initialized")
14- }
15-
16 // List containers using the Docker client13 // List containers using the Docker client
17 containers, err := dockerCli.ContainerList(context.Background(), types.ContainerListOptions{})14 containers, err := dockerCli.ContainerList(context.Background(), types.ContainerListOptions{})
18 if err != nil {15 if err != nil {
@@ -21,3 +18,62 @@ func ListRunningContainers() ([]types.Container, error) {
21 18
22 return containers, nil19 return containers, nil
23 }20 }
21+
22+// StopContainer attempts to stop a container gracefully with a timeout
23+// If it doesn't stop, it sets a flag that can be checked to prompt the user.
24+func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) {
25+ // Check if the Docker client has been initialized
26+ CheckIfInitialized()
27+
28+ // Start by sending a SIGTERM
29+ err := dockerCli.ContainerKill(context.Background(), containerID, "SIGTERM")
30+ if err != nil {
31+ return false, err
32+ }
33+
34+ // Initialize a ticker for timeout
35+ ticker := time.NewTicker(1 * time.Second)
36+ defer ticker.Stop()
37+
38+ for elapsed := 0; elapsed < int(timeoutDuration.Seconds()); elapsed++ {
39+ select {
40+ case <-ticker.C:
41+ // Check if the container is still running
42+ container, err := dockerCli.ContainerInspect(context.Background(), containerID)
43+ if err != nil {
44+ return false, err
45+ }
46+
47+ // If the container is not running, return true indicating it was stopped
48+ if !container.State.Running {
49+ return true, nil
50+ }
51+ }
52+ }
53+
54+ // Return false, signaling that the container needs to be force-stopped
55+ return false, nil
56+}
57+
58+func StopContainerRagefully(containerID string) error {
59+ // Start by sending a SIGKILL
60+ err := dockerCli.ContainerKill(context.Background(), containerID, "SIGKILL")
61+ if err != nil {
62+ return err
63+ }
64+
65+ return nil
66+}
67+
68+func DeleteContainer(containerID string) error {
69+ // Check if the Docker client has been initialized
70+ CheckIfInitialized()
71+
72+ // Delete container using the Docker client
73+ err := dockerCli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{})
74+ if err != nil {
75+ return err
76+ }
77+
78+ return nil
79+}
M pkg/utils/docker/images.go +2 -7
@@ -2,16 +2,13 @@ package docker
22
33 import (
44 "context"
5- "fmt"
65
76 "github.com/docker/docker/api/types"
87 )
98
109 func ListContainerImages() ([]types.ImageSummary, error) {
1110 // Check if the Docker client has been initialized
12- if dockerCli == nil {
13- return nil, fmt.Errorf("Docker client has not been initialized")
14- }
11+ CheckIfInitialized()
1512
1613 // List images using the Docker client
1714 images, err := dockerCli.ImageList(context.Background(), types.ImageListOptions{})
@@ -23,9 +20,7 @@ func ListContainerImages() ([]types.ImageSummary, error) {
2320 }
2421 func DeleteContainerImage(imageID string) error {
2522 // Check if the Docker client has been initialized
26- if dockerCli == nil {
27- return fmt.Errorf("Docker client has not been initialized")
28- }
23+ CheckIfInitialized()
2924
3025 // Delete the image using the Docker client
3126 _, err := dockerCli.ImageRemove(context.Background(), imageID, types.ImageRemoveOptions{})
@@ -2,16 +2,13 @@ package docker
2 2
3 import (3 import (
4 "context"4 "context"
5- "fmt"
6 5
7 "github.com/docker/docker/api/types"6 "github.com/docker/docker/api/types"
8 )7 )
9 8
10 func ListContainerImages() ([]types.ImageSummary, error) {9 func ListContainerImages() ([]types.ImageSummary, error) {
11 // Check if the Docker client has been initialized10 // Check if the Docker client has been initialized
12- if dockerCli == nil {11+ CheckIfInitialized()
13- return nil, fmt.Errorf("Docker client has not been initialized")
14- }
15 12
16 // List images using the Docker client13 // List images using the Docker client
17 images, err := dockerCli.ImageList(context.Background(), types.ImageListOptions{})14 images, err := dockerCli.ImageList(context.Background(), types.ImageListOptions{})
@@ -23,9 +20,7 @@ func ListContainerImages() ([]types.ImageSummary, error) {
23 }20 }
24 func DeleteContainerImage(imageID string) error {21 func DeleteContainerImage(imageID string) error {
25 // Check if the Docker client has been initialized22 // Check if the Docker client has been initialized
26- if dockerCli == nil {23+ CheckIfInitialized()
27- return fmt.Errorf("Docker client has not been initialized")
28- }
29 24
30 // Delete the image using the Docker client25 // Delete the image using the Docker client
31 _, err := dockerCli.ImageRemove(context.Background(), imageID, types.ImageRemoveOptions{})26 _, err := dockerCli.ImageRemove(context.Background(), imageID, types.ImageRemoveOptions{})
M pkg/utils/humanize/humanize.go +7 -1
@@ -42,7 +42,13 @@ func TimeAgo(t time.Time) string {
4242 return fmt.Sprintf("%d minutes ago", int(duration.Minutes()))
4343 case duration < time.Hour*24:
4444 return fmt.Sprintf("%d hours ago", int(duration.Hours()))
45- default:
45+ case duration < time.Hour*24*7:
4646 return fmt.Sprintf("%d days ago", int(duration.Hours()/24))
47+ case duration < time.Hour*24*30:
48+ return fmt.Sprintf("%d weeks ago", int(duration.Hours()/(24*7)))
49+ case duration < time.Hour*24*365:
50+ return fmt.Sprintf("%d months ago", int(duration.Hours()/(24*30)))
51+ default:
52+ return fmt.Sprintf("%d years ago", int(duration.Hours()/(24*365)))
4753 }
4854 }
@@ -42,7 +42,13 @@ func TimeAgo(t time.Time) string {
42 return fmt.Sprintf("%d minutes ago", int(duration.Minutes()))42 return fmt.Sprintf("%d minutes ago", int(duration.Minutes()))
43 case duration < time.Hour*24:43 case duration < time.Hour*24:
44 return fmt.Sprintf("%d hours ago", int(duration.Hours()))44 return fmt.Sprintf("%d hours ago", int(duration.Hours()))
45- default:45+ case duration < time.Hour*24*7:
46 return fmt.Sprintf("%d days ago", int(duration.Hours()/24))46 return fmt.Sprintf("%d days ago", int(duration.Hours()/24))
47+ case duration < time.Hour*24*30:
48+ return fmt.Sprintf("%d weeks ago", int(duration.Hours()/(24*7)))
49+ case duration < time.Hour*24*365:
50+ return fmt.Sprintf("%d months ago", int(duration.Hours()/(24*30)))
51+ default:
52+ return fmt.Sprintf("%d years ago", int(duration.Hours()/(24*365)))
47 }53 }
48 }54 }

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