detached 864f556

Github App integration complete, Also add a a basic manager to interact with docker

commit
864f556aa0a400f263194be0c72cc1f7a6f27f08
author
bnema · · 2023-09-20 18:52 UTC
committer
bnema · · 2023-09-20 21:58 UTC
parent
1a422e8
signature
unsigned
14 files · +317 -106
M cmd/main.go +6 -0
@@ -12,6 +12,7 @@ import (
1212
1313 "github.com/bnema/gordon/internal/app"
1414 "github.com/bnema/gordon/internal/httpserve"
15+ "github.com/bnema/gordon/pkg/utils/docker"
1516 )
1617
1718 func cleanup(a *app.App, memDb *sql.DB) {
@@ -31,6 +32,11 @@ func main() {
3132 // Pass memDb to the app with the rest of the configs
3233 a.DB = memDb
3334
35+ dockerClient := &docker.DockerClient{}
36+ err = dockerClient.InitializeClient(a.Config.NewDockerConfig())
37+ if err != nil {
38+ log.Fatalf("Error initializing Docker client: %s", err)
39+ }
3440 // Setup a channel to capture termination signals
3541 sigs := make(chan os.Signal, 1)
3642 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
@@ -12,6 +12,7 @@ import (
12 12
13 "github.com/bnema/gordon/internal/app"13 "github.com/bnema/gordon/internal/app"
14 "github.com/bnema/gordon/internal/httpserve"14 "github.com/bnema/gordon/internal/httpserve"
15+ "github.com/bnema/gordon/pkg/utils/docker"
15 )16 )
16 17
17 func cleanup(a *app.App, memDb *sql.DB) {18 func cleanup(a *app.App, memDb *sql.DB) {
@@ -31,6 +32,11 @@ func main() {
31 // Pass memDb to the app with the rest of the configs32 // Pass memDb to the app with the rest of the configs
32 a.DB = memDb33 a.DB = memDb
33 34
35+ dockerClient := &docker.DockerClient{}
36+ err = dockerClient.InitializeClient(a.Config.NewDockerConfig())
37+ if err != nil {
38+ log.Fatalf("Error initializing Docker client: %s", err)
39+ }
34 // Setup a channel to capture termination signals40 // Setup a channel to capture termination signals
35 sigs := make(chan os.Signal, 1)41 sigs := make(chan os.Signal, 1)
36 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)42 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
M internal/app/app.go +5 -4
@@ -16,13 +16,14 @@ import (
1616 var (
1717 OauthCallbackURL string
1818 appEnv string
19+ BuildVersion = "0.0.2"
1920 )
2021
2122 func init() {
2223 appEnv = os.Getenv("APP_ENV")
2324 if appEnv == "" {
2425 appEnv = "prod" // Default to "prod" if APP_ENV is not set
25- }
26+ }
2627 }
2728
2829 type App struct {
@@ -44,7 +45,7 @@ type AppConfig struct {
4445
4546 type GeneralConfig struct {
4647 RunEnv string `yaml:"runEnv"`
47- BuildVersion string `yaml:"buildVersion"`
48+ BuildVersion string
4849 }
4950
5051 type HttpConfig struct {
@@ -68,13 +69,13 @@ func InitializeEnvironment() {
6869 fmt.Errorf("Error initializing environment: %s", err)
6970 }
7071 OauthCallbackURL = GenerateOauthCallbackURL(config)
71-
7272 }
7373
7474 func LoadConfig() (AppConfig, error) {
7575 var config AppConfig
7676 configDir, configFile := getConfigFile()
77- fsys := os.DirFS(configDir) // Use directory path here
77+ fsys := os.DirFS(configDir)
78+ BuildVersion = config.General.BuildVersion // Use directory path here
7879 err := parser.OpenYamlFile(fsys, configFile, &config) // Assuming it doesn't need the last argument
7980 return config, err
8081 }
@@ -16,13 +16,14 @@ import (
16 var (16 var (
17 OauthCallbackURL string17 OauthCallbackURL string
18 appEnv string18 appEnv string
19+ BuildVersion = "0.0.2"
19 )20 )
20 21
21 func init() {22 func init() {
22 appEnv = os.Getenv("APP_ENV")23 appEnv = os.Getenv("APP_ENV")
23 if appEnv == "" {24 if appEnv == "" {
24 appEnv = "prod" // Default to "prod" if APP_ENV is not set25 appEnv = "prod" // Default to "prod" if APP_ENV is not set
25- }26+ }
26 }27 }
27 28
28 type App struct {29 type App struct {
@@ -44,7 +45,7 @@ type AppConfig struct {
44 45
45 type GeneralConfig struct {46 type GeneralConfig struct {
46 RunEnv string `yaml:"runEnv"`47 RunEnv string `yaml:"runEnv"`
47- BuildVersion string `yaml:"buildVersion"`48+ BuildVersion string
48 }49 }
49 50
50 type HttpConfig struct {51 type HttpConfig struct {
@@ -68,13 +69,13 @@ func InitializeEnvironment() {
68 fmt.Errorf("Error initializing environment: %s", err)69 fmt.Errorf("Error initializing environment: %s", err)
69 }70 }
70 OauthCallbackURL = GenerateOauthCallbackURL(config)71 OauthCallbackURL = GenerateOauthCallbackURL(config)
71-
72 }72 }
73 73
74 func LoadConfig() (AppConfig, error) {74 func LoadConfig() (AppConfig, error) {
75 var config AppConfig75 var config AppConfig
76 configDir, configFile := getConfigFile()76 configDir, configFile := getConfigFile()
77- fsys := os.DirFS(configDir) // Use directory path here77+ fsys := os.DirFS(configDir)
78+ BuildVersion = config.General.BuildVersion // Use directory path here
78 err := parser.OpenYamlFile(fsys, configFile, &config) // Assuming it doesn't need the last argument79 err := parser.OpenYamlFile(fsys, configFile, &config) // Assuming it doesn't need the last argument
79 return config, err80 return config, err
80 }81 }
M internal/gotemplate/models/html/admin/manager.gohtml +23 -34
@@ -1,42 +1,31 @@
11 <!-- manager.gohtml -->
22 <!DOCTYPE html>
33 <html lang="en">
4+ {{template "header.gohtml" .}}
45
5-{{template "header.gohtml" .}}
6-
7-<body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-8 p-0">
8- {{template "leftmenu.gohtml" .}}
9- <!-- Main content here -->
10- <!-- This div will contain the error message when something goes wrong -->
11- <div id="container-images-error" style="display:none;">
12- Something went wrong. </div>
13- <div class="flex-1 flex flex-col space-y-4 p-4 md:ml-4">
14- <div class="flex flex-col md:flex-row w-full flex-grow md:mt-24">
15- <div class="flex-grow bg-white p-4 rounded-md shadow mb-4">
16- <h1>Container Manager</h1>
17- <!-- List all the images on load with HTMX -->
18- <div id="container-images" hx-get="/image-manager" hx-trigger="load" hx-swap="innerHTML" hx-swap-oob="true"
19- hx-target-error="container-images-error">
20- {{range .Images}}
21- <div class="image-info">
22- <p>ID: {{.ID}}</p>
23- <p>Repository: {{.Repository}}</p>
24- <p>Tag: {{.Tag}}</p>
25- <p>Created: {{.Created}}</p>
26- <p>Size: {{.Size}}</p>
27- </div>
28- {{else}}
29- <p>No images found.</p>
30- {{end}}
6+ <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-8 p-0">
7+ {{template "leftmenu.gohtml" .}}
8+ <!-- Main content here -->
9+ <!-- This div will contain the error message when something goes wrong -->
10+ <div class="flex-1 flex flex-col space-y-4 p-4 md:ml-4">
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">
13+ <h1 class="text-lg md:text-2xl font-bold">Container Manager</h1>
14+ <!-- List all the images on load with HTMX -->
15+ <div
16+ id="container-images"
17+ hx-get="/htmx/image-manager"
18+ hx-trigger="load"
19+ hx-swap="innerHTML"
20+ hx-swap-oob="true"
21+ hx-target-error="container-list-images-error"
22+ ></div>
23+ <div id="container-list-images-error" style="display: none"></div>
3124 </div>
32-
33-
3425 </div>
3526 </div>
36- </div>
37-
38- <!-- footer -->
39- {{template "footer.gohtml" .}}
40-</body>
4127
42-</html>⏎̸
28+ <!-- footer -->
29+ {{template "footer.gohtml" .}}
30+ </body>
31+</html>
@@ -1,42 +1,31 @@
1 <!-- manager.gohtml -->1 <!-- manager.gohtml -->
2 <!DOCTYPE html>2 <!DOCTYPE html>
3 <html lang="en">3 <html lang="en">
4+ {{template "header.gohtml" .}}
4 5
5-{{template "header.gohtml" .}}6+ <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-8 p-0">
6-7+ {{template "leftmenu.gohtml" .}}
7-<body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-8 p-0">8+ <!-- Main content here -->
8- {{template "leftmenu.gohtml" .}}9+ <!-- This div will contain the error message when something goes wrong -->
9- <!-- Main content here -->10+ <div class="flex-1 flex flex-col space-y-4 p-4 md:ml-4">
10- <!-- This div will contain the error message when something goes wrong -->11+ <div class="flex flex-col md:flex-row w-full flex-grow md:mt-24">
11- <div id="container-images-error" style="display:none;">12+ <div class="flex-grow bg-white p-4 rounded-md shadow mb-4">
12- Something went wrong. </div>13+ <h1 class="text-lg md:text-2xl font-bold">Container Manager</h1>
13- <div class="flex-1 flex flex-col space-y-4 p-4 md:ml-4">14+ <!-- List all the images on load with HTMX -->
14- <div class="flex flex-col md:flex-row w-full flex-grow md:mt-24">15+ <div
15- <div class="flex-grow bg-white p-4 rounded-md shadow mb-4">16+ id="container-images"
16- <h1>Container Manager</h1>17+ hx-get="/htmx/image-manager"
17- <!-- List all the images on load with HTMX -->18+ hx-trigger="load"
18- <div id="container-images" hx-get="/image-manager" hx-trigger="load" hx-swap="innerHTML" hx-swap-oob="true"19+ hx-swap="innerHTML"
19- hx-target-error="container-images-error">20+ hx-swap-oob="true"
20- {{range .Images}}21+ hx-target-error="container-list-images-error"
21- <div class="image-info">22+ ></div>
22- <p>ID: {{.ID}}</p>23+ <div id="container-list-images-error" style="display: none"></div>
23- <p>Repository: {{.Repository}}</p>
24- <p>Tag: {{.Tag}}</p>
25- <p>Created: {{.Created}}</p>
26- <p>Size: {{.Size}}</p>
27- </div>
28- {{else}}
29- <p>No images found.</p>
30- {{end}}
31 </div>24 </div>
32-
33-
34 </div>25 </div>
35 </div>26 </div>
36- </div>
37-
38- <!-- footer -->
39- {{template "footer.gohtml" .}}
40-</body>
41 27
42-</html>⏎̸28+ <!-- footer -->
29+ {{template "footer.gohtml" .}}
30+ </body>
31+</html>
R internal/gotemplate/models/html/components.gohtml → internal/gotemplate/models/html/fragments/components.gohtml +0 -0 renamed, 100% similar

renamed, 100% similar

A internal/gotemplate/models/html/fragments/image_list.gohtml +34 -0
@@ -0,0 +1,34 @@
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>
@@ -0,0 +1,34 @@
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>
M internal/gotemplate/models/html/fragments/leftmenu.gohtml +1 -2
@@ -9,8 +9,7 @@
99 </div>
1010 <div class="w-full">
1111 <ul
12- class="flex flex-wrap md:flex-col md:space-y-6 md:py-8 font-semibold text-gray-100 text-lg md:text-2xl md:pl-4 min-w-fit"
13- hx-boost="true">
12+ class="flex flex-wrap md:flex-col md:space-y-6 md:py-8 font-semibold text-gray-100 text-lg md:text-2xl md:pl-4 min-w-fit">
1413 <li class="flex items-center w-1/2 md:w-full">
1514 <i class="iconoir-dashboard-speed mr-2 md:text-3xl"></i>
1615 <a href="/admin">Dashboard</a>
@@ -9,8 +9,7 @@
9 </div>9 </div>
10 <div class="w-full">10 <div class="w-full">
11 <ul11 <ul
12- class="flex flex-wrap md:flex-col md:space-y-6 md:py-8 font-semibold text-gray-100 text-lg md:text-2xl md:pl-4 min-w-fit"12+ class="flex flex-wrap md:flex-col md:space-y-6 md:py-8 font-semibold text-gray-100 text-lg md:text-2xl md:pl-4 min-w-fit">
13- hx-boost="true">
14 <li class="flex items-center w-1/2 md:w-full">13 <li class="flex items-center w-1/2 md:w-full">
15 <i class="iconoir-dashboard-speed mr-2 md:text-3xl"></i>14 <i class="iconoir-dashboard-speed mr-2 md:text-3xl"></i>
16 <a href="/admin">Dashboard</a>15 <a href="/admin">Dashboard</a>
D internal/gotemplate/models/html/fragments/manager.gohtml +0 -0

no textual change.

M internal/httpserve/handler/adminroutes.go +14 -0
@@ -8,6 +8,8 @@ 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"
1113 "github.com/labstack/echo/v4"
1214 )
1315
@@ -60,9 +62,21 @@ func AdminManagerRoute(c echo.Context, a *app.App) error {
6062 return err
6163 }
6264
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+
6376 // Create a data map to pass to the renderer
6477 data := map[string]interface{}{
6578 "CurrentLang": yamlData.CurrentLang,
79+ "Images": images,
6680 }
6781
6882 renderedHTML, err := rendererData.Render(data, a)
@@ -8,6 +8,8 @@ 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"
11 "github.com/labstack/echo/v4"13 "github.com/labstack/echo/v4"
12 )14 )
13 15
@@ -60,9 +62,21 @@ func AdminManagerRoute(c echo.Context, a *app.App) error {
60 return err62 return err
61 }63 }
62 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+
63 // Create a data map to pass to the renderer76 // Create a data map to pass to the renderer
64 data := map[string]interface{}{77 data := map[string]interface{}{
65 "CurrentLang": yamlData.CurrentLang,78 "CurrentLang": yamlData.CurrentLang,
79+ "Images": images,
66 }80 }
67 81
68 renderedHTML, err := rendererData.Render(data, a)82 renderedHTML, err := rendererData.Render(data, a)
M internal/httpserve/handler/docker.go +68 -8
@@ -1,31 +1,91 @@
11 package handler
22
33 import (
4+ "fmt"
45 "net/http"
6+ "time"
57
8+ "github.com/bnema/gordon/internal/app"
9+ "github.com/bnema/gordon/internal/gotemplate/render"
610 "github.com/bnema/gordon/pkg/utils/docker"
11+ "github.com/bnema/gordon/pkg/utils/humanize"
712 "github.com/bnema/gordon/pkg/utils/sanitize"
813 "github.com/labstack/echo/v4"
914 )
1015
11-func ImageManagerHandler(c echo.Context) error {
12- // Retrieve the list of container images.
16+var idMap = make(map[string]string)
17+
18+type HumanReadableContainerImage struct {
19+ docker.ContainerImage
20+ ShortID string
21+ CreatedStr string
22+ SizeStr string
23+}
24+
25+// ImageManagerHandler handles the /image-manager route
26+func ImageManagerHandler(c echo.Context, a *app.App) error {
1327 images, err := docker.ListContainerImages()
1428 if err != nil {
15- rawErrHTML := `<div id="container-images">Error: ` + err.Error() + `</div>`
29+ rawErrHTML := `<div>Error: ` + err.Error() + `</div>`
1630 sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
1731 if err != nil {
18- // Handle the sanitization error
1932 return c.String(http.StatusInternalServerError, "An error occurred during sanitization")
2033 }
2134 return c.HTML(http.StatusInternalServerError, sanitizedHTML)
2235 }
2336
24- // Create a map to hold the data you want to pass to the template.
37+ var humanReadableImages []HumanReadableContainerImage
38+
39+ for _, image := range images {
40+ ShortID := image.ID[7:19]
41+ idMap[ShortID] = image.ID
42+ createdTime := time.Unix(image.Created, 0)
43+ createdStr := humanize.TimeAgo(createdTime)
44+ sizeStr := humanize.BytesToReadableSize(image.Size)
45+
46+ humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{
47+ ShortID: ShortID,
48+ ContainerImage: image,
49+ CreatedStr: createdStr,
50+ SizeStr: sizeStr,
51+ })
52+ }
2553 data := map[string]interface{}{
26- "Images": images,
54+ "Images": humanReadableImages,
2755 }
2856
29- // Render the template with the data.
30- return c.Render(http.StatusOK, "manager", data)
57+ rendererData, err := render.GetHTMLRenderer("html/fragments", "image_list.gohtml", a.TemplateFS, a)
58+ if err != nil {
59+ return err
60+ }
61+ renderedHTML, err := rendererData.Render(data, a)
62+ if err != nil {
63+ return err
64+ }
65+ return c.HTML(200, renderedHTML)
66+}
67+
68+// ImageManagerDeleteHandler handles the /image-manager/delete route
69+func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {
70+ // Get the ShortID from the URL
71+ ShortID := c.Param("ShortID")
72+ // Check if the shortID exists in the idMap
73+ imageID, exists := idMap[ShortID]
74+ if !exists {
75+ return c.String(http.StatusBadRequest, "Invalid shortID")
76+ }
77+ fmt.Println("ShortID:", ShortID, "ImageID:", imageID)
78+
79+ err := docker.DeleteContainerImage(imageID)
80+ if err != nil {
81+ rawErrHTML := `<div>Error: ` + err.Error() + `</div>`
82+ sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
83+ if err != nil {
84+ // Handle the sanitization error
85+ return c.String(http.StatusInternalServerError, "An error occurred during sanitization")
86+ }
87+ return c.HTML(http.StatusInternalServerError, sanitizedHTML)
88+ }
89+ // Since it is HTMX we return a html div with the message "Removed"
90+ return c.HTML(http.StatusOK, `<div>Removed</div>`)
3191 }
@@ -1,31 +1,91 @@
1 package handler1 package handler
2 2
3 import (3 import (
4+ "fmt"
4 "net/http"5 "net/http"
6+ "time"
5 7
8+ "github.com/bnema/gordon/internal/app"
9+ "github.com/bnema/gordon/internal/gotemplate/render"
6 "github.com/bnema/gordon/pkg/utils/docker"10 "github.com/bnema/gordon/pkg/utils/docker"
11+ "github.com/bnema/gordon/pkg/utils/humanize"
7 "github.com/bnema/gordon/pkg/utils/sanitize"12 "github.com/bnema/gordon/pkg/utils/sanitize"
8 "github.com/labstack/echo/v4"13 "github.com/labstack/echo/v4"
9 )14 )
10 15
11-func ImageManagerHandler(c echo.Context) error {16+var idMap = make(map[string]string)
12- // Retrieve the list of container images.17+
18+type HumanReadableContainerImage struct {
19+ docker.ContainerImage
20+ ShortID string
21+ CreatedStr string
22+ SizeStr string
23+}
24+
25+// ImageManagerHandler handles the /image-manager route
26+func ImageManagerHandler(c echo.Context, a *app.App) error {
13 images, err := docker.ListContainerImages()27 images, err := docker.ListContainerImages()
14 if err != nil {28 if err != nil {
15- rawErrHTML := `<div id="container-images">Error: ` + err.Error() + `</div>`29+ rawErrHTML := `<div>Error: ` + err.Error() + `</div>`
16 sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)30 sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
17 if err != nil {31 if err != nil {
18- // Handle the sanitization error
19 return c.String(http.StatusInternalServerError, "An error occurred during sanitization")32 return c.String(http.StatusInternalServerError, "An error occurred during sanitization")
20 }33 }
21 return c.HTML(http.StatusInternalServerError, sanitizedHTML)34 return c.HTML(http.StatusInternalServerError, sanitizedHTML)
22 }35 }
23 36
24- // Create a map to hold the data you want to pass to the template.37+ var humanReadableImages []HumanReadableContainerImage
38+
39+ for _, image := range images {
40+ ShortID := image.ID[7:19]
41+ idMap[ShortID] = image.ID
42+ createdTime := time.Unix(image.Created, 0)
43+ createdStr := humanize.TimeAgo(createdTime)
44+ sizeStr := humanize.BytesToReadableSize(image.Size)
45+
46+ humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{
47+ ShortID: ShortID,
48+ ContainerImage: image,
49+ CreatedStr: createdStr,
50+ SizeStr: sizeStr,
51+ })
52+ }
25 data := map[string]interface{}{53 data := map[string]interface{}{
26- "Images": images,54+ "Images": humanReadableImages,
27 }55 }
28 56
29- // Render the template with the data.57+ rendererData, err := render.GetHTMLRenderer("html/fragments", "image_list.gohtml", a.TemplateFS, a)
30- return c.Render(http.StatusOK, "manager", data)58+ if err != nil {
59+ return err
60+ }
61+ renderedHTML, err := rendererData.Render(data, a)
62+ if err != nil {
63+ return err
64+ }
65+ return c.HTML(200, renderedHTML)
66+}
67+
68+// ImageManagerDeleteHandler handles the /image-manager/delete route
69+func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {
70+ // Get the ShortID from the URL
71+ ShortID := c.Param("ShortID")
72+ // Check if the shortID exists in the idMap
73+ imageID, exists := idMap[ShortID]
74+ if !exists {
75+ return c.String(http.StatusBadRequest, "Invalid shortID")
76+ }
77+ fmt.Println("ShortID:", ShortID, "ImageID:", imageID)
78+
79+ err := docker.DeleteContainerImage(imageID)
80+ if err != nil {
81+ rawErrHTML := `<div>Error: ` + err.Error() + `</div>`
82+ sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
83+ if err != nil {
84+ // Handle the sanitization error
85+ return c.String(http.StatusInternalServerError, "An error occurred during sanitization")
86+ }
87+ return c.HTML(http.StatusInternalServerError, sanitizedHTML)
88+ }
89+ // Since it is HTMX we return a html div with the message "Removed"
90+ return c.HTML(http.StatusOK, `<div>Removed</div>`)
31 }91 }
M internal/httpserve/router.go +10 -1
@@ -26,7 +26,7 @@ func RegisterRoutes(e *echo.Echo, a *app.App) *echo.Echo {
2626 bindAdminRoute(e, a, AdminPath)
2727 bindStaticRoute(e, a, "/*")
2828 bindLoginRoute(e, a, AdminPath)
29- e.GET("/image-manager", handler.ImageManagerHandler)
29+ bindHTMXEndpoints(e, a)
3030 // Protect the root path with a 403
3131 e.GET("/", func(c echo.Context) error {
3232 return c.String(403, "403 Forbidden")
@@ -65,3 +65,12 @@ func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) {
6565 return handler.Logout(c, a)
6666 })
6767 }
68+
69+func bindHTMXEndpoints(e *echo.Echo, a *app.App) {
70+ e.GET("/htmx/image-manager", func(c echo.Context) error {
71+ return handler.ImageManagerHandler(c, a)
72+ })
73+ e.DELETE("/htmx/image-manager/delete/:ShortID", func(c echo.Context) error {
74+ return handler.ImageManagerDeleteHandler(c, a)
75+ })
76+}
@@ -26,7 +26,7 @@ func RegisterRoutes(e *echo.Echo, a *app.App) *echo.Echo {
26 bindAdminRoute(e, a, AdminPath)26 bindAdminRoute(e, a, AdminPath)
27 bindStaticRoute(e, a, "/*")27 bindStaticRoute(e, a, "/*")
28 bindLoginRoute(e, a, AdminPath)28 bindLoginRoute(e, a, AdminPath)
29- e.GET("/image-manager", handler.ImageManagerHandler)29+ bindHTMXEndpoints(e, a)
30 // Protect the root path with a 40330 // Protect the root path with a 403
31 e.GET("/", func(c echo.Context) error {31 e.GET("/", func(c echo.Context) error {
32 return c.String(403, "403 Forbidden")32 return c.String(403, "403 Forbidden")
@@ -65,3 +65,12 @@ func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) {
65 return handler.Logout(c, a)65 return handler.Logout(c, a)
66 })66 })
67 }67 }
68+
69+func bindHTMXEndpoints(e *echo.Echo, a *app.App) {
70+ e.GET("/htmx/image-manager", func(c echo.Context) error {
71+ return handler.ImageManagerHandler(c, a)
72+ })
73+ e.DELETE("/htmx/image-manager/delete/:ShortID", func(c echo.Context) error {
74+ return handler.ImageManagerDeleteHandler(c, a)
75+ })
76+}
M internal/webui/public/assets/css/tailwind.css +55 -14
@@ -558,8 +558,8 @@ video {
558558 margin-right: 0.5rem;
559559 }
560560
561-.mt-20 {
562- margin-top: 5rem;
561+.mt-2 {
562+ margin-top: 0.5rem;
563563 }
564564
565565 .mt-4 {
@@ -602,10 +602,22 @@ video {
602602 width: 50%;
603603 }
604604
605+.w-1\/4 {
606+ width: 25%;
607+}
608+
609+.w-1\/6 {
610+ width: 16.666667%;
611+}
612+
605613 .w-2\/3 {
606614 width: 66.666667%;
607615 }
608616
617+.w-2\/4 {
618+ width: 50%;
619+}
620+
609621 .w-72 {
610622 width: 18rem;
611623 }
@@ -627,10 +639,6 @@ video {
627639 flex-grow: 1;
628640 }
629641
630-.transform {
631- 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));
632-}
633-
634642 .flex-row {
635643 flex-direction: row;
636644 }
@@ -675,6 +683,10 @@ video {
675683 margin-bottom: calc(1rem * var(--tw-space-y-reverse));
676684 }
677685
686+.rounded {
687+ border-radius: 0.25rem;
688+}
689+
678690 .rounded-lg {
679691 border-radius: 0.5rem;
680692 }
@@ -691,6 +703,15 @@ video {
691703 border-width: 1px;
692704 }
693705
706+.border-y-2 {
707+ border-top-width: 2px;
708+ border-bottom-width: 2px;
709+}
710+
711+.border-t-2 {
712+ border-top-width: 2px;
713+}
714+
694715 .border-gray-300 {
695716 --tw-border-opacity: 1;
696717 border-color: rgb(209 213 219 / var(--tw-border-opacity));
@@ -711,6 +732,11 @@ video {
711732 background-color: rgb(209 213 219 / var(--tw-bg-opacity));
712733 }
713734
735+.bg-red-500 {
736+ --tw-bg-opacity: 1;
737+ background-color: rgb(239 68 68 / var(--tw-bg-opacity));
738+}
739+
714740 .bg-transparent {
715741 background-color: transparent;
716742 }
@@ -747,11 +773,20 @@ video {
747773 padding-right: 1rem;
748774 }
749775
776+.py-1 {
777+ padding-top: 0.25rem;
778+ padding-bottom: 0.25rem;
779+}
780+
750781 .py-2 {
751782 padding-top: 0.5rem;
752783 padding-bottom: 0.5rem;
753784 }
754785
786+.align-baseline {
787+ vertical-align: baseline;
788+}
789+
755790 .text-2xl {
756791 font-size: 1.5rem;
757792 line-height: 2rem;
@@ -777,6 +812,10 @@ video {
777812 line-height: 1.25rem;
778813 }
779814
815+.font-bold {
816+ font-weight: 700;
817+}
818+
780819 .font-semibold {
781820 font-weight: 600;
782821 }
@@ -805,6 +844,11 @@ video {
805844 color: rgb(31 41 55 / var(--tw-text-opacity));
806845 }
807846
847+.text-red-800 {
848+ --tw-text-opacity: 1;
849+ color: rgb(153 27 27 / var(--tw-text-opacity));
850+}
851+
808852 .text-slate-100 {
809853 --tw-text-opacity: 1;
810854 color: rgb(241 245 249 / var(--tw-text-opacity));
@@ -826,14 +870,6 @@ video {
826870 outline-offset: 2px;
827871 }
828872
829-.transition {
830- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
831- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
832- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
833- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
834- transition-duration: 150ms;
835-}
836-
837873 /* Import IBM Plex Fonts */
838874
839875 @font-face {
@@ -1045,6 +1081,11 @@ video {
10451081 font-size: 1.875rem;
10461082 line-height: 2.25rem;
10471083 }
1084+
1085+ .md\:text-xl {
1086+ font-size: 1.25rem;
1087+ line-height: 1.75rem;
1088+ }
10481089 }
10491090
10501091 @media (min-width: 1024px) {
@@ -558,8 +558,8 @@ video {
558 margin-right: 0.5rem;558 margin-right: 0.5rem;
559 }559 }
560 560
561-.mt-20 {561+.mt-2 {
562- margin-top: 5rem;562+ margin-top: 0.5rem;
563 }563 }
564 564
565 .mt-4 {565 .mt-4 {
@@ -602,10 +602,22 @@ video {
602 width: 50%;602 width: 50%;
603 }603 }
604 604
605+.w-1\/4 {
606+ width: 25%;
607+}
608+
609+.w-1\/6 {
610+ width: 16.666667%;
611+}
612+
605 .w-2\/3 {613 .w-2\/3 {
606 width: 66.666667%;614 width: 66.666667%;
607 }615 }
608 616
617+.w-2\/4 {
618+ width: 50%;
619+}
620+
609 .w-72 {621 .w-72 {
610 width: 18rem;622 width: 18rem;
611 }623 }
@@ -627,10 +639,6 @@ video {
627 flex-grow: 1;639 flex-grow: 1;
628 }640 }
629 641
630-.transform {
631- 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));
632-}
633-
634 .flex-row {642 .flex-row {
635 flex-direction: row;643 flex-direction: row;
636 }644 }
@@ -675,6 +683,10 @@ video {
675 margin-bottom: calc(1rem * var(--tw-space-y-reverse));683 margin-bottom: calc(1rem * var(--tw-space-y-reverse));
676 }684 }
677 685
686+.rounded {
687+ border-radius: 0.25rem;
688+}
689+
678 .rounded-lg {690 .rounded-lg {
679 border-radius: 0.5rem;691 border-radius: 0.5rem;
680 }692 }
@@ -691,6 +703,15 @@ video {
691 border-width: 1px;703 border-width: 1px;
692 }704 }
693 705
706+.border-y-2 {
707+ border-top-width: 2px;
708+ border-bottom-width: 2px;
709+}
710+
711+.border-t-2 {
712+ border-top-width: 2px;
713+}
714+
694 .border-gray-300 {715 .border-gray-300 {
695 --tw-border-opacity: 1;716 --tw-border-opacity: 1;
696 border-color: rgb(209 213 219 / var(--tw-border-opacity));717 border-color: rgb(209 213 219 / var(--tw-border-opacity));
@@ -711,6 +732,11 @@ video {
711 background-color: rgb(209 213 219 / var(--tw-bg-opacity));732 background-color: rgb(209 213 219 / var(--tw-bg-opacity));
712 }733 }
713 734
735+.bg-red-500 {
736+ --tw-bg-opacity: 1;
737+ background-color: rgb(239 68 68 / var(--tw-bg-opacity));
738+}
739+
714 .bg-transparent {740 .bg-transparent {
715 background-color: transparent;741 background-color: transparent;
716 }742 }
@@ -747,11 +773,20 @@ video {
747 padding-right: 1rem;773 padding-right: 1rem;
748 }774 }
749 775
776+.py-1 {
777+ padding-top: 0.25rem;
778+ padding-bottom: 0.25rem;
779+}
780+
750 .py-2 {781 .py-2 {
751 padding-top: 0.5rem;782 padding-top: 0.5rem;
752 padding-bottom: 0.5rem;783 padding-bottom: 0.5rem;
753 }784 }
754 785
786+.align-baseline {
787+ vertical-align: baseline;
788+}
789+
755 .text-2xl {790 .text-2xl {
756 font-size: 1.5rem;791 font-size: 1.5rem;
757 line-height: 2rem;792 line-height: 2rem;
@@ -777,6 +812,10 @@ video {
777 line-height: 1.25rem;812 line-height: 1.25rem;
778 }813 }
779 814
815+.font-bold {
816+ font-weight: 700;
817+}
818+
780 .font-semibold {819 .font-semibold {
781 font-weight: 600;820 font-weight: 600;
782 }821 }
@@ -805,6 +844,11 @@ video {
805 color: rgb(31 41 55 / var(--tw-text-opacity));844 color: rgb(31 41 55 / var(--tw-text-opacity));
806 }845 }
807 846
847+.text-red-800 {
848+ --tw-text-opacity: 1;
849+ color: rgb(153 27 27 / var(--tw-text-opacity));
850+}
851+
808 .text-slate-100 {852 .text-slate-100 {
809 --tw-text-opacity: 1;853 --tw-text-opacity: 1;
810 color: rgb(241 245 249 / var(--tw-text-opacity));854 color: rgb(241 245 249 / var(--tw-text-opacity));
@@ -826,14 +870,6 @@ video {
826 outline-offset: 2px;870 outline-offset: 2px;
827 }871 }
828 872
829-.transition {
830- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
831- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
832- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
833- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
834- transition-duration: 150ms;
835-}
836-
837 /* Import IBM Plex Fonts */873 /* Import IBM Plex Fonts */
838 874
839 @font-face {875 @font-face {
@@ -1045,6 +1081,11 @@ video {
1045 font-size: 1.875rem;1081 font-size: 1.875rem;
1046 line-height: 2.25rem;1082 line-height: 2.25rem;
1047 }1083 }
1084+
1085+ .md\:text-xl {
1086+ font-size: 1.25rem;
1087+ line-height: 1.75rem;
1088+ }
1048 }1089 }
1049 1090
1050 @media (min-width: 1024px) {1091 @media (min-width: 1024px) {
M pkg/utils/docker/initclient.go +52 -42
@@ -3,21 +3,21 @@ package docker
33 import (
44 "context"
55 "fmt"
6- "net"
7- "net/http"
8- "net/url"
9- "time"
106
117 "github.com/docker/docker/api/types"
128 "github.com/docker/docker/client"
139 )
1410
1511 type ContainerImage struct {
16- ID string
17- Repository string
18- Tag string
19- Created time.Time
20- Size int64
12+ ID string `json:"Id"`
13+ ParentID string `json:",omitempty"`
14+ RepoTags []string `json:",omitempty"`
15+ RepoDigests []string `json:",omitempty"`
16+ Created int64
17+ Size int64
18+ SharedSize int64
19+ Labels map[string]string `json:",omitempty"`
20+ Containers int64
2121 }
2222
2323 type ContainerEngineClient interface {
@@ -36,26 +36,19 @@ var dockerCli *client.Client
3636 type DockerClient struct{}
3737
3838 func (d *DockerClient) InitializeClient(config *Config) error {
39- // Create a custom HTTP client that uses the Docker socket
40- httpClient := &http.Client{
41- Transport: &http.Transport{
42- DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
43- return net.Dial("unix", config.Sock)
44- },
45- },
39+ // Validate if the Sock field is not empty
40+ if config.Sock == "" {
41+ return fmt.Errorf("Sock field in Config is empty")
4642 }
4743
4844 // Initialize Docker client
4945 cli, err := client.NewClientWithOpts(
50- client.WithHTTPClient(httpClient),
5146 client.WithAPIVersionNegotiation(),
52- client.WithScheme("unix"),
53- client.WithHost(url.PathEscape(config.Sock)),
47+ client.WithHost("unix://"+config.Sock), // Prepend "unix://" to the Unix socket path
5448 )
5549 if err != nil {
56- return err
50+ return fmt.Errorf("Error from DockerClient: %s", err)
5751 }
58-
5952 dockerCli = cli
6053 return nil
6154 }
@@ -76,30 +69,47 @@ func ListContainerImages() ([]ContainerImage, error) {
7669 var containerImages []ContainerImage
7770 for _, image := range images {
7871 containerImages = append(containerImages, ContainerImage{
79- ID: image.ID,
80- // Repository and Tag information may require additional processing
81- Created: time.Unix(image.Created, 0),
82- Size: image.Size,
72+ ID: image.ID,
73+ ParentID: image.ParentID,
74+ RepoTags: image.RepoTags,
75+ RepoDigests: image.RepoDigests,
76+ Created: image.Created,
77+ Size: image.Size,
78+ SharedSize: image.SharedSize,
79+ Labels: image.Labels,
80+ Containers: image.Containers,
8381 })
8482 }
8583
8684 return containerImages, nil
8785 }
8886
89-// // PodmanClient implements the ContainerEngineClient interface for Podman
90-// type PodmanClient struct{}
91-
92-// func (p *PodmanClient) InitializeClient(config *Config) error {
93-// // Initialize Podman client here
94-// return nil // Return an error if something goes wrong
95-// }
96-// func InitializeEngineClient(config *Config) {
97-// if config.PodmanEnable {
98-// engineClient = &PodmanClient{}
99-// } else {
100-// engineClient = &DockerClient{}
101-// }
102-// if err := engineClient.InitializeClient(config); err != nil {
103-// log.Fatalf("Error initializing container engine client: %v", err)
104-// }
105-// }
87+func ListRunningContainers() ([]types.Container, error) {
88+ // Check if the Docker client has been initialized
89+ if dockerCli == nil {
90+ return nil, fmt.Errorf("Docker client has not been initialized")
91+ }
92+
93+ // List containers using the Docker client
94+ containers, err := dockerCli.ContainerList(context.Background(), types.ContainerListOptions{})
95+ if err != nil {
96+ return nil, err
97+ }
98+
99+ return containers, nil
100+}
101+
102+func DeleteContainerImage(imageID string) error {
103+ // Check if the Docker client has been initialized
104+ if dockerCli == nil {
105+ return fmt.Errorf("Docker client has not been initialized")
106+ }
107+
108+ // Delete the image using the Docker client
109+ _, err := dockerCli.ImageRemove(context.Background(), imageID, types.ImageRemoveOptions{})
110+ if err != nil {
111+ return err
112+ }
113+
114+ return nil
115+}
@@ -3,21 +3,21 @@ package docker
3 import (3 import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6- "net"
7- "net/http"
8- "net/url"
9- "time"
10 6
11 "github.com/docker/docker/api/types"7 "github.com/docker/docker/api/types"
12 "github.com/docker/docker/client"8 "github.com/docker/docker/client"
13 )9 )
14 10
15 type ContainerImage struct {11 type ContainerImage struct {
16- ID string12+ ID string `json:"Id"`
17- Repository string13+ ParentID string `json:",omitempty"`
18- Tag string14+ RepoTags []string `json:",omitempty"`
19- Created time.Time15+ RepoDigests []string `json:",omitempty"`
20- Size int6416+ Created int64
17+ Size int64
18+ SharedSize int64
19+ Labels map[string]string `json:",omitempty"`
20+ Containers int64
21 }21 }
22 22
23 type ContainerEngineClient interface {23 type ContainerEngineClient interface {
@@ -36,26 +36,19 @@ var dockerCli *client.Client
36 type DockerClient struct{}36 type DockerClient struct{}
37 37
38 func (d *DockerClient) InitializeClient(config *Config) error {38 func (d *DockerClient) InitializeClient(config *Config) error {
39- // Create a custom HTTP client that uses the Docker socket39+ // Validate if the Sock field is not empty
40- httpClient := &http.Client{40+ if config.Sock == "" {
41- Transport: &http.Transport{41+ return fmt.Errorf("Sock field in Config is empty")
42- DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
43- return net.Dial("unix", config.Sock)
44- },
45- },
46 }42 }
47 43
48 // Initialize Docker client44 // Initialize Docker client
49 cli, err := client.NewClientWithOpts(45 cli, err := client.NewClientWithOpts(
50- client.WithHTTPClient(httpClient),
51 client.WithAPIVersionNegotiation(),46 client.WithAPIVersionNegotiation(),
52- client.WithScheme("unix"),47+ client.WithHost("unix://"+config.Sock), // Prepend "unix://" to the Unix socket path
53- client.WithHost(url.PathEscape(config.Sock)),
54 )48 )
55 if err != nil {49 if err != nil {
56- return err50+ return fmt.Errorf("Error from DockerClient: %s", err)
57 }51 }
58-
59 dockerCli = cli52 dockerCli = cli
60 return nil53 return nil
61 }54 }
@@ -76,30 +69,47 @@ func ListContainerImages() ([]ContainerImage, error) {
76 var containerImages []ContainerImage69 var containerImages []ContainerImage
77 for _, image := range images {70 for _, image := range images {
78 containerImages = append(containerImages, ContainerImage{71 containerImages = append(containerImages, ContainerImage{
79- ID: image.ID,72+ ID: image.ID,
80- // Repository and Tag information may require additional processing73+ ParentID: image.ParentID,
81- Created: time.Unix(image.Created, 0),74+ RepoTags: image.RepoTags,
82- Size: image.Size,75+ RepoDigests: image.RepoDigests,
76+ Created: image.Created,
77+ Size: image.Size,
78+ SharedSize: image.SharedSize,
79+ Labels: image.Labels,
80+ Containers: image.Containers,
83 })81 })
84 }82 }
85 83
86 return containerImages, nil84 return containerImages, nil
87 }85 }
88 86
89-// // PodmanClient implements the ContainerEngineClient interface for Podman87+func ListRunningContainers() ([]types.Container, error) {
90-// type PodmanClient struct{}88+ // Check if the Docker client has been initialized
91-89+ if dockerCli == nil {
92-// func (p *PodmanClient) InitializeClient(config *Config) error {90+ return nil, fmt.Errorf("Docker client has not been initialized")
93-// // Initialize Podman client here91+ }
94-// return nil // Return an error if something goes wrong92+
95-// }93+ // List containers using the Docker client
96-// func InitializeEngineClient(config *Config) {94+ containers, err := dockerCli.ContainerList(context.Background(), types.ContainerListOptions{})
97-// if config.PodmanEnable {95+ if err != nil {
98-// engineClient = &PodmanClient{}96+ return nil, err
99-// } else {97+ }
100-// engineClient = &DockerClient{}98+
101-// }99+ return containers, nil
102-// if err := engineClient.InitializeClient(config); err != nil {100+}
103-// log.Fatalf("Error initializing container engine client: %v", err)101+
104-// }102+func DeleteContainerImage(imageID string) error {
105-// }103+ // Check if the Docker client has been initialized
104+ if dockerCli == nil {
105+ return fmt.Errorf("Docker client has not been initialized")
106+ }
107+
108+ // Delete the image using the Docker client
109+ _, err := dockerCli.ImageRemove(context.Background(), imageID, types.ImageRemoveOptions{})
110+ if err != nil {
111+ return err
112+ }
113+
114+ return nil
115+}
M pkg/utils/htmx/htmlfragments.go +1 -1
@@ -12,7 +12,7 @@ import (
1212 // GetHTMLFragmentByID returns the HTML fragment with the specified id
1313 func GetHTMLFragmentByID(id string, data interface{}, a *app.App) (string, error) {
1414 // 1. Render the template
15- renderer, err := render.GetHTMLRenderer("components.gohtml", a.TemplateFS, a)
15+ renderer, err := render.GetHTMLRenderer("html/fragments", "components.gohtml", a.TemplateFS, a)
1616 if err != nil {
1717 return "", fmt.Errorf("failed to get renderer: %w", err)
1818 }
@@ -12,7 +12,7 @@ import (
12 // GetHTMLFragmentByID returns the HTML fragment with the specified id12 // GetHTMLFragmentByID returns the HTML fragment with the specified id
13 func GetHTMLFragmentByID(id string, data interface{}, a *app.App) (string, error) {13 func GetHTMLFragmentByID(id string, data interface{}, a *app.App) (string, error) {
14 // 1. Render the template14 // 1. Render the template
15- renderer, err := render.GetHTMLRenderer("components.gohtml", a.TemplateFS, a)15+ renderer, err := render.GetHTMLRenderer("html/fragments", "components.gohtml", a.TemplateFS, a)
16 if err != nil {16 if err != nil {
17 return "", fmt.Errorf("failed to get renderer: %w", err)17 return "", fmt.Errorf("failed to get renderer: %w", err)
18 }18 }
A pkg/utils/humanize/humanize.go +48 -0
@@ -0,0 +1,48 @@
1+package humanize
2+
3+import (
4+ "fmt"
5+ "time"
6+)
7+
8+func BytesToReadableSize(size int64) string {
9+ var unit string
10+ var converted float64
11+
12+ if size >= 1<<30 {
13+ unit = "GB"
14+ converted = float64(size) / (1 << 30)
15+ } else if size >= 1<<20 {
16+ unit = "MB"
17+ converted = float64(size) / (1 << 20)
18+ } else if size >= 1<<10 {
19+ unit = "KB"
20+ converted = float64(size) / (1 << 10)
21+ } else {
22+ unit = "B"
23+ converted = float64(size)
24+ }
25+
26+ return fmt.Sprintf("%.2f%s", converted, unit)
27+}
28+
29+func TimeAgo(t time.Time) string {
30+ var duration time.Duration
31+ now := time.Now()
32+ if t.After(now) {
33+ duration = t.Sub(now)
34+ } else {
35+ duration = now.Sub(t)
36+ }
37+
38+ switch {
39+ case duration < time.Minute:
40+ return fmt.Sprintf("%d seconds ago", int(duration.Seconds()))
41+ case duration < time.Hour:
42+ return fmt.Sprintf("%d minutes ago", int(duration.Minutes()))
43+ case duration < time.Hour*24:
44+ return fmt.Sprintf("%d hours ago", int(duration.Hours()))
45+ default:
46+ return fmt.Sprintf("%d days ago", int(duration.Hours()/24))
47+ }
48+}
@@ -0,0 +1,48 @@
1+package humanize
2+
3+import (
4+ "fmt"
5+ "time"
6+)
7+
8+func BytesToReadableSize(size int64) string {
9+ var unit string
10+ var converted float64
11+
12+ if size >= 1<<30 {
13+ unit = "GB"
14+ converted = float64(size) / (1 << 30)
15+ } else if size >= 1<<20 {
16+ unit = "MB"
17+ converted = float64(size) / (1 << 20)
18+ } else if size >= 1<<10 {
19+ unit = "KB"
20+ converted = float64(size) / (1 << 10)
21+ } else {
22+ unit = "B"
23+ converted = float64(size)
24+ }
25+
26+ return fmt.Sprintf("%.2f%s", converted, unit)
27+}
28+
29+func TimeAgo(t time.Time) string {
30+ var duration time.Duration
31+ now := time.Now()
32+ if t.After(now) {
33+ duration = t.Sub(now)
34+ } else {
35+ duration = now.Sub(t)
36+ }
37+
38+ switch {
39+ case duration < time.Minute:
40+ return fmt.Sprintf("%d seconds ago", int(duration.Seconds()))
41+ case duration < time.Hour:
42+ return fmt.Sprintf("%d minutes ago", int(duration.Minutes()))
43+ case duration < time.Hour*24:
44+ return fmt.Sprintf("%d hours ago", int(duration.Hours()))
45+ default:
46+ return fmt.Sprintf("%d days ago", int(duration.Hours()/24))
47+ }
48+}

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