detached a5a2735

New view for the manager + some refactoring

For containers:
- Add a view to list all the containiers
- Start/Stop/Remove a container
- Error handling

For the images :
- List all available images
- Remove button
- Error handling
commit
a5a2735bac590f8ab4742aa481677caa8c539d7b
author
bnema · · 2023-09-22 14:38 UTC
committer
bnema · · 2023-09-22 14:46 UTC
parent
478146d
signature
unsigned
13 files · +346 -207
M cmd/srv/main.go +1 -1
@@ -35,7 +35,7 @@ func main() {
3535 dockerClient := &docker.DockerClient{}
3636 err = dockerClient.InitializeClient(a.Config.NewDockerConfig())
3737 if err != nil {
38- log.Fatalf("Error initializing Docker client: %s", err)
38+ log.Printf("Error from DockerClient: %s", err)
3939 }
4040 // Setup a channel to capture termination signals
4141 sigs := make(chan os.Signal, 1)
@@ -35,7 +35,7 @@ func main() {
35 dockerClient := &docker.DockerClient{}35 dockerClient := &docker.DockerClient{}
36 err = dockerClient.InitializeClient(a.Config.NewDockerConfig())36 err = dockerClient.InitializeClient(a.Config.NewDockerConfig())
37 if err != nil {37 if err != nil {
38- log.Fatalf("Error initializing Docker client: %s", err)38+ log.Printf("Error from DockerClient: %s", err)
39 }39 }
40 // Setup a channel to capture termination signals40 // Setup a channel to capture termination signals
41 sigs := make(chan os.Signal, 1)41 sigs := make(chan os.Signal, 1)
M internal/app/app.go +11 -3
@@ -59,8 +59,9 @@ type AdminConfig struct {
5959 }
6060
6161 type ContainerEngineConfig struct {
62- Sock string `yaml:"sock"`
63- PodmanEnable bool `yaml:"podman"`
62+ Sock string `yaml:"dockersock"`
63+ PodmanSock string `yaml:"podmansock"`
64+ Podman bool `yaml:"podman"`
6465 }
6566
6667 func InitializeEnvironment() {
@@ -104,9 +105,16 @@ func NewApp() *App {
104105
105106 // NewDockerConfig creates and returns a new Docker client configuration based on AppConfig.
106107 func (config *AppConfig) NewDockerConfig() *docker.Config {
108+ if config.ContainerEngine.Podman {
109+ return &docker.Config{
110+ Sock: config.ContainerEngine.PodmanSock,
111+ PodmanEnable: true,
112+ }
113+ }
107114 return &docker.Config{
108115 Sock: config.ContainerEngine.Sock,
109- PodmanEnable: config.ContainerEngine.PodmanEnable}
116+ PodmanEnable: false,
117+ }
110118 }
111119
112120 func GenerateOauthCallbackURL(config AppConfig) string {
@@ -59,8 +59,9 @@ type AdminConfig struct {
59 }59 }
60 60
61 type ContainerEngineConfig struct {61 type ContainerEngineConfig struct {
62- Sock string `yaml:"sock"`62+ Sock string `yaml:"dockersock"`
63- PodmanEnable bool `yaml:"podman"`63+ PodmanSock string `yaml:"podmansock"`
64+ Podman bool `yaml:"podman"`
64 }65 }
65 66
66 func InitializeEnvironment() {67 func InitializeEnvironment() {
@@ -104,9 +105,16 @@ func NewApp() *App {
104 105
105 // NewDockerConfig creates and returns a new Docker client configuration based on AppConfig.106 // NewDockerConfig creates and returns a new Docker client configuration based on AppConfig.
106 func (config *AppConfig) NewDockerConfig() *docker.Config {107 func (config *AppConfig) NewDockerConfig() *docker.Config {
108+ if config.ContainerEngine.Podman {
109+ return &docker.Config{
110+ Sock: config.ContainerEngine.PodmanSock,
111+ PodmanEnable: true,
112+ }
113+ }
107 return &docker.Config{114 return &docker.Config{
108 Sock: config.ContainerEngine.Sock,115 Sock: config.ContainerEngine.Sock,
109- PodmanEnable: config.ContainerEngine.PodmanEnable}116+ PodmanEnable: false,
117+ }
110 }118 }
111 119
112 func GenerateOauthCallbackURL(config AppConfig) string {120 func GenerateOauthCallbackURL(config AppConfig) string {
M internal/gotemplate/models/html/admin/manager.gohtml +2 -1
@@ -6,7 +6,6 @@
66 <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-8 p-0">
77 {{template "leftmenu.gohtml" .}}
88 <!-- Main content here -->
9- <!-- This div will contain the error message when something goes wrong -->
109 <div class="flex-1 flex flex-col space-y-4 p-4 md:ml-4">
1110 <div class="flex flex-col md:flex-row w-full flex-grow md:mt-24">
1211 <div class="flex-grow bg-white p-4 rounded-md shadow mb-4">
@@ -19,6 +18,7 @@
1918 hx-swap="innerHTML"
2019 hx-swap-oob="true"
2120 hx-target-error="container-manager-error"
21+ hx-poll="5s"
2222 ></div>
2323 <div id="container-manager-error" style="display: none"></div>
2424 <!-- List all the images -->
@@ -29,6 +29,7 @@
2929 hx-swap="innerHTML"
3030 hx-swap-oob="true"
3131 hx-target-error="image-manager-error"
32+ hx-poll="5s"
3233 ></div>
3334 <div id="image-manager-error" style="display: none"></div>
3435 </div>
@@ -6,7 +6,6 @@
6 <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-8 p-0">6 <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-8 p-0">
7 {{template "leftmenu.gohtml" .}}7 {{template "leftmenu.gohtml" .}}
8 <!-- Main content here -->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">9 <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">10 <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">11 <div class="flex-grow bg-white p-4 rounded-md shadow mb-4">
@@ -19,6 +18,7 @@
19 hx-swap="innerHTML"18 hx-swap="innerHTML"
20 hx-swap-oob="true"19 hx-swap-oob="true"
21 hx-target-error="container-manager-error"20 hx-target-error="container-manager-error"
21+ hx-poll="5s"
22 ></div>22 ></div>
23 <div id="container-manager-error" style="display: none"></div>23 <div id="container-manager-error" style="display: none"></div>
24 <!-- List all the images -->24 <!-- List all the images -->
@@ -29,6 +29,7 @@
29 hx-swap="innerHTML"29 hx-swap="innerHTML"
30 hx-swap-oob="true"30 hx-swap-oob="true"
31 hx-target-error="image-manager-error"31 hx-target-error="image-manager-error"
32+ hx-poll="5s"
32 ></div>33 ></div>
33 <div id="image-manager-error" style="display: none"></div>34 <div id="image-manager-error" style="display: none"></div>
34 </div>35 </div>
M internal/gotemplate/models/html/fragments/containerlist.gohtml +80 -43
@@ -1,46 +1,83 @@
11 <div class="flex flex-col p-4">
22 <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>
3+ <table class="p-4 border-t-2" hx-ext="response-targets">
4+ <!-- <thead>
5+ <tr>
6+ <th>Name</th>
7+ <th>Command</th>
8+ <th>Created</th>
9+ <th>Status</th>
10+ <th>Port</th>
11+ <th>State</th>
12+ <th>Action</th>
13+ </tr>
14+ </thead> -->
15+ <tbody>
16+ {{
17+ range.containers
18+ }}
19+ <tr id="container-{{.ID}}">
20+ <td>
21+ <span title="{{.Container.ID}}">{{.Name}}</span>
22+ </td>
23+ <td class="command-container" style="text-decoration: underline">
24+ <span title="{{.Command}}">{{.Command}}</span>
25+ </td>
26+ <td>{{.CreatedStr}}</td>
27+ <td class="port-container">{{ .Ports }}</td>
28+ <td class="status-container">{{.Status}} (Up since {{.UpSince}})</td>
29+ <td style="color: {{.StateColor}};">{{.State}}</td>
30+ <td align="right">
31+ <!-- iconoir start -->
32+ <button
33+ id="start-button-{{.ID}}"
34+ hx-post="/htmx/container-manager/start/{{.ID}}"
35+ hx-target="#container-{{.ID}}"
36+ hx-target-*="#error-message-{{.ID}}"
37+ hx-swap="innerHTML"
38+ class="py-2 px-4"
39+ >
40+ <span class="iconoir-play text-xl font-bold"></span>
41+ </button>
42+ <!-- stop -->
43+ <button
44+ id="stop-button-{{.ID}}"
45+ hx-post="/htmx/container-manager/stop/{{.ID}}"
46+ hx-target="#container-{{.ID}}"
47+ hx-target-*="#error-message-{{.ID}}"
48+ hx-swap="innerHTML"
49+ hx-confirm="Are you sure you want to stop this container?"
50+ class="py-2 px-4"
51+ >
52+ <span class="iconoir-system-shut text-xl font-bold"></span>
53+ </button>
54+ <!-- REMOVE -->
55+ <button
56+ id="remove-button-img-{{.ID}}"
57+ hx-delete="/htmx/container-manager/delete/{{.ID}}"
58+ hx-target="#container-{{.ID}}"
59+ hx-target-*="#error-message-{{.ID}}"
60+ hx-swap="innerHTML"
61+ hx-confirm="Are you sure you want to delete this container image?"
62+ class="py-2 px-4"
63+ >
64+ <!-- iconoir trash -->
65+ <span class="iconoir-trash text-xl text-red-600 font-bold"></span>
66+ </button>
67+ </td>
68+ </tr>
69+ <tr>
70+ <td colspan="7">
71+ <div id="error-message-{{.ID}}" class="text-red-500"></div>
72+ </td>
73+ </tr>
74+ {{ else }}
75+ <tr>
76+ <td colspan="7" class="mt-2">No Running containers.</td>
77+ </tr>
78+ {{
79+ end
80+ }}
81+ </tbody>
82+ </table>
4683 </div>
@@ -1,46 +1,83 @@
1 <div class="flex flex-col p-4">1 <div class="flex flex-col p-4">
2 <h1 class="text-lg md:text-xl font-bold">Running containers</h1>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">3+ <table class="p-4 border-t-2" hx-ext="response-targets">
4- <div class="w-1/6">Name</div>4+ <!-- <thead>
5- <div class="w-1/6">Command</div>5+ <tr>
6- <div class="w-1/6">Created</div>6+ <th>Name</th>
7- <div class="w-1/6">Status</div>7+ <th>Command</th>
8- <div class="w-1/6">Port</div>8+ <th>Created</th>
9- <div class="w-1/6">State</div>9+ <th>Status</th>
10- <div class="w-1/4">Action</div>10+ <th>Port</th>
11- </div>11+ <th>State</th>
12- {{ range.Containers }}12+ <th>Action</th>
13- <div id="{{.ID}}" class="flex py-2 align-baseline justify-center">13+ </tr>
14- <div class="w-1/6">{{ range.Names }}{{.}}<br />{{ end }}</div>14+ </thead> -->
15- <div class="w-1/6" style="text-decoration: underline">{{.Command}}</div>15+ <tbody>
16- <div class="w-1/6">{{.CreatedStr}}</div>16+ {{
17- <div class="w-1/6">{{.Status}} (Up since {{.UpSince}})</div>17+ range.containers
18- <div class="w-1/6">{{ range.Ports }}{{.}}<br />{{ end }}</div>18+ }}
19- <div class="w-1/6" style="color: {{.StateColor}};">{{.State}}</div>19+ <tr id="container-{{.ID}}">
20- <div class="w-1/4">20+ <td>
21- <div x-data="{ success: false, loading: false }">21+ <span title="{{.Container.ID}}">{{.Name}}</span>
22- <button22+ </td>
23- id="remove-button-{{.ShortID}}"23+ <td class="command-container" style="text-decoration: underline">
24- x-bind:class="{ 'bg-green-500': success, 'bg-red-500': !success, 'text-white': true, 'px-2': true, 'py-1': true, 'rounded': true }"24+ <span title="{{.Command}}">{{.Command}}</span>
25- @click="loading = true"25+ </td>
26- hx-trigger="click"26+ <td>{{.CreatedStr}}</td>
27- hx-delete="/htmx/container-manager/delete/{{.ShortID}}"27+ <td class="port-container">{{ .Ports }}</td>
28- hx-swap="outerHTML swap:outerHTML"28+ <td class="status-container">{{.Status}} (Up since {{.UpSince}})</td>
29- hx-target-error="container-delete-images-error"29+ <td style="color: {{.StateColor}};">{{.State}}</td>
30- x-on:htmx:afterSwap="if ($event.detail.status === 200 && $event.detail.xhr.response === 'Success') { success = true; loading = false; setTimeout(() => location.reload(), 3000); }"30+ <td align="right">
31- >31+ <!-- iconoir start -->
32- <span x-show="!loading && !success">Remove</span>32+ <button
33- <span x-show="success">Success</span>33+ id="start-button-{{.ID}}"
34- </button>34+ hx-post="/htmx/container-manager/start/{{.ID}}"
35- </div>35+ hx-target="#container-{{.ID}}"
36- </div>36+ hx-target-*="#error-message-{{.ID}}"
37- </div>37+ hx-swap="innerHTML"
38- {{ else }}38+ class="py-2 px-4"
39- <div class="mt-2">No Running containers.</div>39+ >
40- {{ end }}40+ <span class="iconoir-play text-xl font-bold"></span>
41- <div41+ </button>
42- id="container-delete-error"42+ <!-- stop -->
43- style="display: none"43+ <button
44- class="mt-2 text-red-800 text-lg"44+ id="stop-button-{{.ID}}"
45- ></div>45+ hx-post="/htmx/container-manager/stop/{{.ID}}"
46+ hx-target="#container-{{.ID}}"
47+ hx-target-*="#error-message-{{.ID}}"
48+ hx-swap="innerHTML"
49+ hx-confirm="Are you sure you want to stop this container?"
50+ class="py-2 px-4"
51+ >
52+ <span class="iconoir-system-shut text-xl font-bold"></span>
53+ </button>
54+ <!-- REMOVE -->
55+ <button
56+ id="remove-button-img-{{.ID}}"
57+ hx-delete="/htmx/container-manager/delete/{{.ID}}"
58+ hx-target="#container-{{.ID}}"
59+ hx-target-*="#error-message-{{.ID}}"
60+ hx-swap="innerHTML"
61+ hx-confirm="Are you sure you want to delete this container image?"
62+ class="py-2 px-4"
63+ >
64+ <!-- iconoir trash -->
65+ <span class="iconoir-trash text-xl text-red-600 font-bold"></span>
66+ </button>
67+ </td>
68+ </tr>
69+ <tr>
70+ <td colspan="7">
71+ <div id="error-message-{{.ID}}" class="text-red-500"></div>
72+ </td>
73+ </tr>
74+ {{ else }}
75+ <tr>
76+ <td colspan="7" class="mt-2">No Running containers.</td>
77+ </tr>
78+ {{
79+ end
80+ }}
81+ </tbody>
82+ </table>
46 </div>83 </div>
M internal/gotemplate/models/html/fragments/imagelist.gohtml +50 -30
@@ -1,32 +1,52 @@
1-<div class="flex flex-col p-4" hx-ext="response-targets">
1+<div class="flex flex-col p-4">
22 <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="img-{{.ShortImgID}}" class="flex py-2 align-baseline justify-center">
11- <div class="w-2/4">
12- <span title="{{.ImageSummary.ID}}">{{.Name}}</span>
13- </div>
14- <div class="w-2/4">{{.CreatedStr}}</div>
15- <div class="w-1/6">{{.SizeStr}}</div>
16- <button
17- id="remove-button-img-{{.ShortImgID}}"
18- hx-delete="/htmx/image-manager/delete/{{.ShortImgID}}"
19- hx-target="#img-{{.ShortImgID}}"
20- hx-target-*="#error-message-img"
21- hx-swap="innerHTML"
22- hx-confirm="Are you sure you want to delete this container image?"
23- class="w-1/4 bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-md"
24- >
25- Delete
26- </button>
27- </div>
28- <div id="error-message-img" class="mt-2 text-red-500" display="none"></div>
29- {{ else }}
30- <div class="mt-2">No images found.</div>
31- {{ end }}
3+ <table class="p-4 border-t-2">
4+ <!-- <thead>
5+ <tr>
6+ <th>Name</th>
7+ <th>Created</th>
8+ <th>Size</th>
9+ <th>Action</th>
10+ </tr>
11+ </thead> -->
12+ <tbody>
13+ {{
14+ range.Images
15+ }}
16+ <tr id="img-{{.ID}}" hx-ext="response-targets">
17+ <td>
18+ <span title="{{.ImageSummary.ID}}">{{.Name}}</span>
19+ </td>
20+ <td>
21+ <span title="Image creation date">{{.CreatedStr}}</span>
22+ </td>
23+ <td>{{.SizeStr}}</td>
24+ <td align="right">
25+ <button
26+ id="remove-button-img-{{.ID}}"
27+ hx-delete="/htmx/image-manager/delete/{{.ID}}"
28+ hx-target="#img-{{.ID}}"
29+ hx-target-*="#error-message-div-{{.ID}}"
30+ hx-swap="innerHTML"
31+ hx-confirm="Are you sure you want to delete this container image?"
32+ class="py-2 px-4"
33+ >
34+ <span class="iconoir-trash text-xl text-red-600 font-bold"></span>
35+ </button>
36+ </td>
37+ </tr>
38+ <t>
39+ <td colspan="4">
40+ <div id="error-message-div-{{.ID}}" class="text-red-500"></div>
41+ </td>
42+ </tr>
43+ {{ else }}
44+ <tr>
45+ <td colspan="4" class="mt-2">No images found.</td>
46+ </tr>
47+ {{
48+ end
49+ }}
50+ </tbody>
51+ </table>
3252 </div>
@@ -1,32 +1,52 @@
1-<div class="flex flex-col p-4" hx-ext="response-targets">1+<div class="flex flex-col p-4">
2 <h1 class="text-lg md:text-xl font-bold">Images</h1>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">3+ <table class="p-4 border-t-2">
4- <div class="w-2/4">Name</div>4+ <!-- <thead>
5- <div class="w-2/4">Created</div>5+ <tr>
6- <div class="w-1/6">Size</div>6+ <th>Name</th>
7- <div class="w-1/4">Action</div>7+ <th>Created</th>
8- </div>8+ <th>Size</th>
9- {{ range.Images }}9+ <th>Action</th>
10- <div id="img-{{.ShortImgID}}" class="flex py-2 align-baseline justify-center">10+ </tr>
11- <div class="w-2/4">11+ </thead> -->
12- <span title="{{.ImageSummary.ID}}">{{.Name}}</span>12+ <tbody>
13- </div>13+ {{
14- <div class="w-2/4">{{.CreatedStr}}</div>14+ range.Images
15- <div class="w-1/6">{{.SizeStr}}</div>15+ }}
16- <button16+ <tr id="img-{{.ID}}" hx-ext="response-targets">
17- id="remove-button-img-{{.ShortImgID}}"17+ <td>
18- hx-delete="/htmx/image-manager/delete/{{.ShortImgID}}"18+ <span title="{{.ImageSummary.ID}}">{{.Name}}</span>
19- hx-target="#img-{{.ShortImgID}}"19+ </td>
20- hx-target-*="#error-message-img"20+ <td>
21- hx-swap="innerHTML"21+ <span title="Image creation date">{{.CreatedStr}}</span>
22- hx-confirm="Are you sure you want to delete this container image?"22+ </td>
23- class="w-1/4 bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-md"23+ <td>{{.SizeStr}}</td>
24- >24+ <td align="right">
25- Delete25+ <button
26- </button>26+ id="remove-button-img-{{.ID}}"
27- </div>27+ hx-delete="/htmx/image-manager/delete/{{.ID}}"
28- <div id="error-message-img" class="mt-2 text-red-500" display="none"></div>28+ hx-target="#img-{{.ID}}"
29- {{ else }}29+ hx-target-*="#error-message-div-{{.ID}}"
30- <div class="mt-2">No images found.</div>30+ hx-swap="innerHTML"
31- {{ end }}31+ hx-confirm="Are you sure you want to delete this container image?"
32+ class="py-2 px-4"
33+ >
34+ <span class="iconoir-trash text-xl text-red-600 font-bold"></span>
35+ </button>
36+ </td>
37+ </tr>
38+ <t>
39+ <td colspan="4">
40+ <div id="error-message-div-{{.ID}}" class="text-red-500"></div>
41+ </td>
42+ </tr>
43+ {{ else }}
44+ <tr>
45+ <td colspan="4" class="mt-2">No images found.</td>
46+ </tr>
47+ {{
48+ end
49+ }}
50+ </tbody>
51+ </table>
32 </div>52 </div>
M internal/httpserve/handler/docker.go +88 -45
@@ -3,29 +3,33 @@ package handler
33 import (
44 "fmt"
55 "net/http"
6+ "sync"
67 "time"
78
89 "github.com/bnema/gordon/internal/app"
910 "github.com/bnema/gordon/internal/gotemplate/render"
1011 "github.com/bnema/gordon/pkg/utils/docker"
1112 "github.com/bnema/gordon/pkg/utils/humanize"
12- "github.com/bnema/gordon/pkg/utils/sanitize"
1313 "github.com/docker/docker/api/types"
1414 "github.com/labstack/echo/v4"
1515 )
1616
1717 var idMap = make(map[string]string)
18+var idMapMutex sync.Mutex
1819
1920 type HumanReadableContainerImage struct {
2021 *types.ImageSummary
2122 Name string
22- ShortImgID string
23+ ShortID string
2324 CreatedStr string
2425 SizeStr string
2526 }
2627
2728 type HumanReadableContainer struct {
2829 *types.Container
30+ Name string
31+ Ports []string
32+ ShortID string
2933 CreatedStr string // Human-readable Created time
3034 SizeStr string // Human-readable Size
3135 UpSince string // Human-readable time elapsed since the container was started
@@ -36,29 +40,26 @@ type HumanReadableContainer struct {
3640 func ImageManagerHandler(c echo.Context, a *app.App) error {
3741 images, err := docker.ListContainerImages()
3842 if err != nil {
39- rawErrHTML := `<div>Error: ` + err.Error() + `</div>`
40- sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
41- if err != nil {
42- return c.String(http.StatusInternalServerError, "An error occurred during sanitization")
43- }
44- return c.HTML(http.StatusInternalServerError, sanitizedHTML)
43+ return sendError(c, err)
4544 }
46-
4745 var humanReadableImages []HumanReadableContainerImage
4846
4947 for _, image := range images {
50- ShortImgID := image.ID[7:19]
51- idMap[ShortImgID] = image.ID
48+ ShortID := image.ID[7:19]
49+ idMapMutex.Lock()
50+ idMap[ShortID] = image.ID
51+ idMapMutex.Unlock()
5252 createdTime := time.Unix(image.Created, 0)
5353 createdStr := humanize.TimeAgo(createdTime)
5454 sizeStr := humanize.BytesToReadableSize(image.Size)
5555 for _, repoTag := range image.RepoTags {
5656 humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{
57- ShortImgID: ShortImgID,
58- ImageSummary: &image,
59- CreatedStr: createdStr,
60- SizeStr: sizeStr,
61- Name: repoTag,
57+ ImageSummary: &types.ImageSummary{
58+ ID: ShortID, // Set the ID to the ShortID
59+ },
60+ CreatedStr: createdStr,
61+ SizeStr: sizeStr,
62+ Name: repoTag,
6263 })
6364 }
6465 }
@@ -80,22 +81,29 @@ func ImageManagerHandler(c echo.Context, a *app.App) error {
8081 // ImageManagerDeleteHandler handles the /image-manager/delete route
8182 func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {
8283 // Get the ShortImgID from the URL
83- ShortImgID := c.Param("ShortImgID")
84- fmt.Println("ShortImgID:", ShortImgID)
84+ ShortID := c.Param("ID")
85+
86+ idMapMutex.Lock()
8587 // Check if the ShortImgID exists in the idMap
86- imageID, exists := idMap[ShortImgID]
87- fmt.Println("ImageID:", imageID)
88+ imageID, exists := idMap[ShortID]
89+ idMapMutex.Unlock()
90+
8891 if !exists {
8992 return c.String(http.StatusBadRequest, "Invalid ShortImgID")
9093 }
94+
9195 err := docker.DeleteContainerImage(imageID)
9296 if err != nil {
9397 c.Response().Header().Set("X-Error-Type", "image")
9498 return c.String(http.StatusInternalServerError, err.Error())
95-
9699 }
97100
98- // Since it is HTMX we return a html div with the message "Removed"
101+ // Remove the entry from idMap
102+ idMapMutex.Lock()
103+ delete(idMap, ShortID)
104+ idMapMutex.Unlock()
105+
106+ // Since it is HTMX we return a HTML div with the message "Removed"
99107 return c.HTML(http.StatusOK, `<div>Removed</div>`)
100108 }
101109
@@ -103,16 +111,44 @@ func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {
103111 func ContainerManagerHandler(c echo.Context, a *app.App) error {
104112 containers, err := docker.ListRunningContainers()
105113 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)
114+ return sendError(c, err)
112115 }
116+ // We need Name / Command / Created / Status / Ports / State
117+ var humanReadableContainers []HumanReadableContainer
118+
119+ for _, container := range containers {
120+ ShortID := container.ID[0:12]
121+ idMap[ShortID] = container.ID
122+ createdTime := time.Unix(container.Created, 0)
123+ createdStr := humanize.TimeAgo(createdTime)
124+ sizeStr := humanize.BytesToReadableSize(container.SizeRw)
125+ stateColor := "green"
126+ // If the container is exited, set the state color to red
127+ if container.State == "exited" {
128+ stateColor = "red"
129+ }
130+ var ports []string
131+ for _, port := range container.Ports {
132+ portStr := fmt.Sprintf("%d:%d", port.PublicPort, port.PrivatePort)
133+ ports = append(ports, portStr)
134+ }
113135
136+ for _, name := range container.Names {
137+ name = name[1:]
138+ humanReadableContainers = append(humanReadableContainers, HumanReadableContainer{
139+ Container: &container,
140+ CreatedStr: createdStr,
141+ SizeStr: sizeStr,
142+ UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)),
143+ StateColor: stateColor,
144+ Name: name,
145+ ShortID: ShortID,
146+ Ports: ports,
147+ })
148+ }
149+ }
114150 data := map[string]interface{}{
115- "Containers": containers,
151+ "containers": humanReadableContainers,
116152 }
117153
118154 rendererData, err := render.GetHTMLRenderer("html/fragments", "containerlist.gohtml", a.TemplateFS, a)
@@ -128,25 +164,32 @@ func ContainerManagerHandler(c echo.Context, a *app.App) error {
128164
129165 // ContainerManagerDeleteHandler handles the /container-manager/delete route
130166 func ContainerManagerDeleteHandler(c echo.Context, a *app.App) error {
131- // Get the ShortID from the URL
132- ShortID := c.Param("ShortID")
133- // Check if the shortID exists in the idMap
134- containerID, exists := idMap[ShortID]
135- if !exists {
136- return c.String(http.StatusBadRequest, "Invalid shortID")
167+ ID := c.Param("ID")
168+ err := docker.DeleteContainer(ID)
169+ if err != nil {
170+ return sendError(c, err)
137171 }
138- fmt.Println("ShortID:", ShortID, "ContainerID:", containerID)
172+ // Since it is HTMX we return a html div with the message "Removed"
173+ return c.HTML(http.StatusOK, `Success`)
174+}
139175
140- err := docker.DeleteContainer(containerID)
176+// ContainerManagerStopHandler handles the /container-manager/stop route
177+func ContainerManagerStopHandler(c echo.Context, a *app.App) error {
178+ ID := c.Param("ID")
179+ // Stop the container gracefully with a timeout of 10 seconds
180+ stopped, err := docker.StopContainerGracefully(ID, 3*time.Second)
141181 if err != nil {
142- rawErrHTML := `<div>Error: ` + err.Error() + `</div>`
143- sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
144- if err != nil {
145- // Handle the sanitization error
146- return c.String(http.StatusInternalServerError, "An error occurred during sanitization")
147- }
148- return c.HTML(http.StatusInternalServerError, sanitizedHTML)
182+ return sendError(c, err)
149183 }
150- // Since it is HTMX we return a html div with the message "Removed"
184+ // if stopped is true, the container was stopped gracefully
185+ if stopped {
186+ return c.HTML(http.StatusOK, `Success`)
187+ }
188+ // If stopped is false, the container was not stopped gracefully we force stop it
189+ err = docker.StopContainerRagefully(ID)
190+ if err != nil {
191+ return sendError(c, err)
192+ }
193+
151194 return c.HTML(http.StatusOK, `Success`)
152195 }
@@ -3,29 +3,33 @@ package handler
3 import (3 import (
4 "fmt"4 "fmt"
5 "net/http"5 "net/http"
6+ "sync"
6 "time"7 "time"
7 8
8 "github.com/bnema/gordon/internal/app"9 "github.com/bnema/gordon/internal/app"
9 "github.com/bnema/gordon/internal/gotemplate/render"10 "github.com/bnema/gordon/internal/gotemplate/render"
10 "github.com/bnema/gordon/pkg/utils/docker"11 "github.com/bnema/gordon/pkg/utils/docker"
11 "github.com/bnema/gordon/pkg/utils/humanize"12 "github.com/bnema/gordon/pkg/utils/humanize"
12- "github.com/bnema/gordon/pkg/utils/sanitize"
13 "github.com/docker/docker/api/types"13 "github.com/docker/docker/api/types"
14 "github.com/labstack/echo/v4"14 "github.com/labstack/echo/v4"
15 )15 )
16 16
17 var idMap = make(map[string]string)17 var idMap = make(map[string]string)
18+var idMapMutex sync.Mutex
18 19
19 type HumanReadableContainerImage struct {20 type HumanReadableContainerImage struct {
20 *types.ImageSummary21 *types.ImageSummary
21 Name string22 Name string
22- ShortImgID string23+ ShortID string
23 CreatedStr string24 CreatedStr string
24 SizeStr string25 SizeStr string
25 }26 }
26 27
27 type HumanReadableContainer struct {28 type HumanReadableContainer struct {
28 *types.Container29 *types.Container
30+ Name string
31+ Ports []string
32+ ShortID string
29 CreatedStr string // Human-readable Created time33 CreatedStr string // Human-readable Created time
30 SizeStr string // Human-readable Size34 SizeStr string // Human-readable Size
31 UpSince string // Human-readable time elapsed since the container was started35 UpSince string // Human-readable time elapsed since the container was started
@@ -36,29 +40,26 @@ type HumanReadableContainer struct {
36 func ImageManagerHandler(c echo.Context, a *app.App) error {40 func ImageManagerHandler(c echo.Context, a *app.App) error {
37 images, err := docker.ListContainerImages()41 images, err := docker.ListContainerImages()
38 if err != nil {42 if err != nil {
39- rawErrHTML := `<div>Error: ` + err.Error() + `</div>`43+ return sendError(c, err)
40- sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
41- if err != nil {
42- return c.String(http.StatusInternalServerError, "An error occurred during sanitization")
43- }
44- return c.HTML(http.StatusInternalServerError, sanitizedHTML)
45 }44 }
46-
47 var humanReadableImages []HumanReadableContainerImage45 var humanReadableImages []HumanReadableContainerImage
48 46
49 for _, image := range images {47 for _, image := range images {
50- ShortImgID := image.ID[7:19]48+ ShortID := image.ID[7:19]
51- idMap[ShortImgID] = image.ID49+ idMapMutex.Lock()
50+ idMap[ShortID] = image.ID
51+ idMapMutex.Unlock()
52 createdTime := time.Unix(image.Created, 0)52 createdTime := time.Unix(image.Created, 0)
53 createdStr := humanize.TimeAgo(createdTime)53 createdStr := humanize.TimeAgo(createdTime)
54 sizeStr := humanize.BytesToReadableSize(image.Size)54 sizeStr := humanize.BytesToReadableSize(image.Size)
55 for _, repoTag := range image.RepoTags {55 for _, repoTag := range image.RepoTags {
56 humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{56 humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{
57- ShortImgID: ShortImgID,57+ ImageSummary: &types.ImageSummary{
58- ImageSummary: &image,58+ ID: ShortID, // Set the ID to the ShortID
59- CreatedStr: createdStr,59+ },
60- SizeStr: sizeStr,60+ CreatedStr: createdStr,
61- Name: repoTag,61+ SizeStr: sizeStr,
62+ Name: repoTag,
62 })63 })
63 }64 }
64 }65 }
@@ -80,22 +81,29 @@ func ImageManagerHandler(c echo.Context, a *app.App) error {
80 // ImageManagerDeleteHandler handles the /image-manager/delete route81 // ImageManagerDeleteHandler handles the /image-manager/delete route
81 func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {82 func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {
82 // Get the ShortImgID from the URL83 // Get the ShortImgID from the URL
83- ShortImgID := c.Param("ShortImgID")84+ ShortID := c.Param("ID")
84- fmt.Println("ShortImgID:", ShortImgID)85+
86+ idMapMutex.Lock()
85 // Check if the ShortImgID exists in the idMap87 // Check if the ShortImgID exists in the idMap
86- imageID, exists := idMap[ShortImgID]88+ imageID, exists := idMap[ShortID]
87- fmt.Println("ImageID:", imageID)89+ idMapMutex.Unlock()
90+
88 if !exists {91 if !exists {
89 return c.String(http.StatusBadRequest, "Invalid ShortImgID")92 return c.String(http.StatusBadRequest, "Invalid ShortImgID")
90 }93 }
94+
91 err := docker.DeleteContainerImage(imageID)95 err := docker.DeleteContainerImage(imageID)
92 if err != nil {96 if err != nil {
93 c.Response().Header().Set("X-Error-Type", "image")97 c.Response().Header().Set("X-Error-Type", "image")
94 return c.String(http.StatusInternalServerError, err.Error())98 return c.String(http.StatusInternalServerError, err.Error())
95-
96 }99 }
97 100
98- // Since it is HTMX we return a html div with the message "Removed"101+ // Remove the entry from idMap
102+ idMapMutex.Lock()
103+ delete(idMap, ShortID)
104+ idMapMutex.Unlock()
105+
106+ // Since it is HTMX we return a HTML div with the message "Removed"
99 return c.HTML(http.StatusOK, `<div>Removed</div>`)107 return c.HTML(http.StatusOK, `<div>Removed</div>`)
100 }108 }
101 109
@@ -103,16 +111,44 @@ func ImageManagerDeleteHandler(c echo.Context, a *app.App) error {
103 func ContainerManagerHandler(c echo.Context, a *app.App) error {111 func ContainerManagerHandler(c echo.Context, a *app.App) error {
104 containers, err := docker.ListRunningContainers()112 containers, err := docker.ListRunningContainers()
105 if err != nil {113 if err != nil {
106- rawErrHTML := `<div>Error: ` + err.Error() + `</div>`114+ return sendError(c, err)
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 }115 }
116+ // We need Name / Command / Created / Status / Ports / State
117+ var humanReadableContainers []HumanReadableContainer
118+
119+ for _, container := range containers {
120+ ShortID := container.ID[0:12]
121+ idMap[ShortID] = container.ID
122+ createdTime := time.Unix(container.Created, 0)
123+ createdStr := humanize.TimeAgo(createdTime)
124+ sizeStr := humanize.BytesToReadableSize(container.SizeRw)
125+ stateColor := "green"
126+ // If the container is exited, set the state color to red
127+ if container.State == "exited" {
128+ stateColor = "red"
129+ }
130+ var ports []string
131+ for _, port := range container.Ports {
132+ portStr := fmt.Sprintf("%d:%d", port.PublicPort, port.PrivatePort)
133+ ports = append(ports, portStr)
134+ }
113 135
136+ for _, name := range container.Names {
137+ name = name[1:]
138+ humanReadableContainers = append(humanReadableContainers, HumanReadableContainer{
139+ Container: &container,
140+ CreatedStr: createdStr,
141+ SizeStr: sizeStr,
142+ UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)),
143+ StateColor: stateColor,
144+ Name: name,
145+ ShortID: ShortID,
146+ Ports: ports,
147+ })
148+ }
149+ }
114 data := map[string]interface{}{150 data := map[string]interface{}{
115- "Containers": containers,151+ "containers": humanReadableContainers,
116 }152 }
117 153
118 rendererData, err := render.GetHTMLRenderer("html/fragments", "containerlist.gohtml", a.TemplateFS, a)154 rendererData, err := render.GetHTMLRenderer("html/fragments", "containerlist.gohtml", a.TemplateFS, a)
@@ -128,25 +164,32 @@ func ContainerManagerHandler(c echo.Context, a *app.App) error {
128 164
129 // ContainerManagerDeleteHandler handles the /container-manager/delete route165 // ContainerManagerDeleteHandler handles the /container-manager/delete route
130 func ContainerManagerDeleteHandler(c echo.Context, a *app.App) error {166 func ContainerManagerDeleteHandler(c echo.Context, a *app.App) error {
131- // Get the ShortID from the URL167+ ID := c.Param("ID")
132- ShortID := c.Param("ShortID")168+ err := docker.DeleteContainer(ID)
133- // Check if the shortID exists in the idMap169+ if err != nil {
134- containerID, exists := idMap[ShortID]170+ return sendError(c, err)
135- if !exists {
136- return c.String(http.StatusBadRequest, "Invalid shortID")
137 }171 }
138- fmt.Println("ShortID:", ShortID, "ContainerID:", containerID)172+ // Since it is HTMX we return a html div with the message "Removed"
173+ return c.HTML(http.StatusOK, `Success`)
174+}
139 175
140- err := docker.DeleteContainer(containerID)176+// ContainerManagerStopHandler handles the /container-manager/stop route
177+func ContainerManagerStopHandler(c echo.Context, a *app.App) error {
178+ ID := c.Param("ID")
179+ // Stop the container gracefully with a timeout of 10 seconds
180+ stopped, err := docker.StopContainerGracefully(ID, 3*time.Second)
141 if err != nil {181 if err != nil {
142- rawErrHTML := `<div>Error: ` + err.Error() + `</div>`182+ return sendError(c, err)
143- sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
144- if err != nil {
145- // Handle the sanitization error
146- return c.String(http.StatusInternalServerError, "An error occurred during sanitization")
147- }
148- return c.HTML(http.StatusInternalServerError, sanitizedHTML)
149 }183 }
150- // Since it is HTMX we return a html div with the message "Removed"184+ // if stopped is true, the container was stopped gracefully
185+ if stopped {
186+ return c.HTML(http.StatusOK, `Success`)
187+ }
188+ // If stopped is false, the container was not stopped gracefully we force stop it
189+ err = docker.StopContainerRagefully(ID)
190+ if err != nil {
191+ return sendError(c, err)
192+ }
193+
151 return c.HTML(http.StatusOK, `Success`)194 return c.HTML(http.StatusOK, `Success`)
152 }195 }
A internal/httpserve/handler/senderror.go +17 -0
@@ -0,0 +1,17 @@
1+package handler
2+
3+import (
4+ "net/http"
5+
6+ "github.com/bnema/gordon/pkg/utils/sanitize"
7+ "github.com/labstack/echo/v4"
8+)
9+
10+func sendError(c echo.Context, err error) error {
11+ rawErrHTML := `<div>Error: ` + err.Error() + `</div>`
12+ sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
13+ if err != nil {
14+ return c.String(http.StatusInternalServerError, "An error occurred during sanitization")
15+ }
16+ return c.HTML(http.StatusInternalServerError, sanitizedHTML)
17+}
@@ -0,0 +1,17 @@
1+package handler
2+
3+import (
4+ "net/http"
5+
6+ "github.com/bnema/gordon/pkg/utils/sanitize"
7+ "github.com/labstack/echo/v4"
8+)
9+
10+func sendError(c echo.Context, err error) error {
11+ rawErrHTML := `<div>Error: ` + err.Error() + `</div>`
12+ sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML)
13+ if err != nil {
14+ return c.String(http.StatusInternalServerError, "An error occurred during sanitization")
15+ }
16+ return c.HTML(http.StatusInternalServerError, sanitizedHTML)
17+}
M internal/httpserve/router.go +6 -2
@@ -70,14 +70,18 @@ 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/:ShortImgID", func(c echo.Context) error {
73+ e.DELETE("/htmx/image-manager/delete/:ID", func(c echo.Context) error {
7474 return handler.ImageManagerDeleteHandler(c, a)
7575 })
7676
7777 e.GET("/htmx/container-manager", func(c echo.Context) error {
7878 return handler.ContainerManagerHandler(c, a)
7979 })
80- e.DELETE("/htmx/container-manager/delete/:ShortID", func(c echo.Context) error {
80+ e.POST("/htmx/container-manager/stop/:ID", func(c echo.Context) error {
81+ return handler.ContainerManagerStopHandler(c, a)
82+ })
83+
84+ e.DELETE("/htmx/container-manager/delete/:ID", func(c echo.Context) error {
8185 return handler.ContainerManagerDeleteHandler(c, a)
8286 })
8387 }
@@ -70,14 +70,18 @@ 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/:ShortImgID", func(c echo.Context) error {73+ e.DELETE("/htmx/image-manager/delete/:ID", func(c echo.Context) error {
74 return handler.ImageManagerDeleteHandler(c, a)74 return handler.ImageManagerDeleteHandler(c, a)
75 })75 })
76 76
77 e.GET("/htmx/container-manager", func(c echo.Context) error {77 e.GET("/htmx/container-manager", func(c echo.Context) error {
78 return handler.ContainerManagerHandler(c, a)78 return handler.ContainerManagerHandler(c, a)
79 })79 })
80- e.DELETE("/htmx/container-manager/delete/:ShortID", func(c echo.Context) error {80+ e.POST("/htmx/container-manager/stop/:ID", func(c echo.Context) error {
81+ return handler.ContainerManagerStopHandler(c, a)
82+ })
83+
84+ e.DELETE("/htmx/container-manager/delete/:ID", func(c echo.Context) error {
81 return handler.ContainerManagerDeleteHandler(c, a)85 return handler.ContainerManagerDeleteHandler(c, a)
82 })86 })
83 }87 }
M internal/webui/public/assets/css/tailwind.css +79 -46
@@ -575,6 +575,10 @@ video {
575575 }
576576 }
577577
578+.relative {
579+ position: relative;
580+}
581+
578582 .my-2 {
579583 margin-top: 0.5rem;
580584 margin-bottom: 0.5rem;
@@ -592,18 +596,22 @@ video {
592596 margin-right: 0.5rem;
593597 }
594598
595-.mt-2 {
596- margin-top: 0.5rem;
597-}
598-
599599 .mt-4 {
600600 margin-top: 1rem;
601601 }
602602
603+.mt-2 {
604+ margin-top: 0.5rem;
605+}
606+
603607 .flex {
604608 display: flex;
605609 }
606610
611+.table {
612+ display: table;
613+}
614+
607615 .hidden {
608616 display: none;
609617 }
@@ -636,22 +644,10 @@ video {
636644 width: 50%;
637645 }
638646
639-.w-1\/4 {
640- width: 25%;
641-}
642-
643-.w-1\/6 {
644- width: 16.666667%;
645-}
646-
647647 .w-2\/3 {
648648 width: 66.666667%;
649649 }
650650
651-.w-2\/4 {
652- width: 50%;
653-}
654-
655651 .w-72 {
656652 width: 18rem;
657653 }
@@ -660,6 +656,18 @@ video {
660656 width: 100%;
661657 }
662658
659+.w-1\/4 {
660+ width: 25%;
661+}
662+
663+.w-1\/6 {
664+ width: 16.666667%;
665+}
666+
667+.w-2\/4 {
668+ width: 50%;
669+}
670+
663671 .min-w-fit {
664672 min-width: -moz-fit-content;
665673 min-width: fit-content;
@@ -717,10 +725,6 @@ video {
717725 margin-bottom: calc(1rem * var(--tw-space-y-reverse));
718726 }
719727
720-.rounded {
721- border-radius: 0.25rem;
722-}
723-
724728 .rounded-lg {
725729 border-radius: 0.5rem;
726730 }
@@ -733,6 +737,10 @@ video {
733737 border-radius: 0.125rem;
734738 }
735739
740+.rounded {
741+ border-radius: 0.25rem;
742+}
743+
736744 .border {
737745 border-width: 1px;
738746 }
@@ -746,6 +754,11 @@ video {
746754 border-color: rgb(209 213 219 / var(--tw-border-opacity));
747755 }
748756
757+.border-red-400 {
758+ --tw-border-opacity: 1;
759+ border-color: rgb(248 113 113 / var(--tw-border-opacity));
760+}
761+
749762 .bg-gordonmascot-hblue {
750763 --tw-bg-opacity: 1;
751764 background-color: rgb(1 30 92 / var(--tw-bg-opacity));
@@ -761,16 +774,6 @@ video {
761774 background-color: rgb(209 213 219 / var(--tw-bg-opacity));
762775 }
763776
764-.bg-green-500 {
765- --tw-bg-opacity: 1;
766- background-color: rgb(34 197 94 / var(--tw-bg-opacity));
767-}
768-
769-.bg-red-500 {
770- --tw-bg-opacity: 1;
771- background-color: rgb(239 68 68 / var(--tw-bg-opacity));
772-}
773-
774777 .bg-transparent {
775778 background-color: transparent;
776779 }
@@ -785,6 +788,21 @@ video {
785788 background-color: rgb(228 228 231 / var(--tw-bg-opacity));
786789 }
787790
791+.bg-green-500 {
792+ --tw-bg-opacity: 1;
793+ background-color: rgb(34 197 94 / var(--tw-bg-opacity));
794+}
795+
796+.bg-red-500 {
797+ --tw-bg-opacity: 1;
798+ background-color: rgb(239 68 68 / var(--tw-bg-opacity));
799+}
800+
801+.bg-red-100 {
802+ --tw-bg-opacity: 1;
803+ background-color: rgb(254 226 226 / var(--tw-bg-opacity));
804+}
805+
788806 .p-0 {
789807 padding: 0px;
790808 }
@@ -807,14 +825,19 @@ video {
807825 padding-right: 1rem;
808826 }
809827
828+.py-2 {
829+ padding-top: 0.5rem;
830+ padding-bottom: 0.5rem;
831+}
832+
810833 .py-1 {
811834 padding-top: 0.25rem;
812835 padding-bottom: 0.25rem;
813836 }
814837
815-.py-2 {
816- padding-top: 0.5rem;
817- padding-bottom: 0.5rem;
838+.py-3 {
839+ padding-top: 0.75rem;
840+ padding-bottom: 0.75rem;
818841 }
819842
820843 .align-baseline {
@@ -846,14 +869,19 @@ video {
846869 line-height: 1.25rem;
847870 }
848871
849-.font-bold {
850- font-weight: 700;
872+.text-xl {
873+ font-size: 1.25rem;
874+ line-height: 1.75rem;
851875 }
852876
853877 .font-semibold {
854878 font-weight: 600;
855879 }
856880
881+.font-bold {
882+ font-weight: 700;
883+}
884+
857885 .tracking-tighter {
858886 letter-spacing: -0.05em;
859887 }
@@ -878,9 +906,14 @@ video {
878906 color: rgb(31 41 55 / var(--tw-text-opacity));
879907 }
880908
881-.text-red-500 {
909+.text-slate-100 {
882910 --tw-text-opacity: 1;
883- color: rgb(239 68 68 / var(--tw-text-opacity));
911+ color: rgb(241 245 249 / var(--tw-text-opacity));
912+}
913+
914+.text-white {
915+ --tw-text-opacity: 1;
916+ color: rgb(255 255 255 / var(--tw-text-opacity));
884917 }
885918
886919 .text-red-800 {
@@ -888,14 +921,19 @@ video {
888921 color: rgb(153 27 27 / var(--tw-text-opacity));
889922 }
890923
891-.text-slate-100 {
924+.text-red-700 {
892925 --tw-text-opacity: 1;
893- color: rgb(241 245 249 / var(--tw-text-opacity));
926+ color: rgb(185 28 28 / var(--tw-text-opacity));
894927 }
895928
896-.text-white {
929+.text-red-500 {
897930 --tw-text-opacity: 1;
898- color: rgb(255 255 255 / var(--tw-text-opacity));
931+ color: rgb(239 68 68 / var(--tw-text-opacity));
932+}
933+
934+.text-red-600 {
935+ --tw-text-opacity: 1;
936+ color: rgb(220 38 38 / var(--tw-text-opacity));
899937 }
900938
901939 .underline {
@@ -1033,11 +1071,6 @@ video {
10331071 margin-right: 8px;
10341072 }
10351073
1036-.hover\:bg-red-700:hover {
1037- --tw-bg-opacity: 1;
1038- background-color: rgb(185 28 28 / var(--tw-bg-opacity));
1039-}
1040-
10411074 @media (min-width: 640px) {
10421075 .sm\:h-24 {
10431076 height: 6rem;
@@ -575,6 +575,10 @@ video {
575 }575 }
576 }576 }
577 577
578+.relative {
579+ position: relative;
580+}
581+
578 .my-2 {582 .my-2 {
579 margin-top: 0.5rem;583 margin-top: 0.5rem;
580 margin-bottom: 0.5rem;584 margin-bottom: 0.5rem;
@@ -592,18 +596,22 @@ video {
592 margin-right: 0.5rem;596 margin-right: 0.5rem;
593 }597 }
594 598
595-.mt-2 {
596- margin-top: 0.5rem;
597-}
598-
599 .mt-4 {599 .mt-4 {
600 margin-top: 1rem;600 margin-top: 1rem;
601 }601 }
602 602
603+.mt-2 {
604+ margin-top: 0.5rem;
605+}
606+
603 .flex {607 .flex {
604 display: flex;608 display: flex;
605 }609 }
606 610
611+.table {
612+ display: table;
613+}
614+
607 .hidden {615 .hidden {
608 display: none;616 display: none;
609 }617 }
@@ -636,22 +644,10 @@ video {
636 width: 50%;644 width: 50%;
637 }645 }
638 646
639-.w-1\/4 {
640- width: 25%;
641-}
642-
643-.w-1\/6 {
644- width: 16.666667%;
645-}
646-
647 .w-2\/3 {647 .w-2\/3 {
648 width: 66.666667%;648 width: 66.666667%;
649 }649 }
650 650
651-.w-2\/4 {
652- width: 50%;
653-}
654-
655 .w-72 {651 .w-72 {
656 width: 18rem;652 width: 18rem;
657 }653 }
@@ -660,6 +656,18 @@ video {
660 width: 100%;656 width: 100%;
661 }657 }
662 658
659+.w-1\/4 {
660+ width: 25%;
661+}
662+
663+.w-1\/6 {
664+ width: 16.666667%;
665+}
666+
667+.w-2\/4 {
668+ width: 50%;
669+}
670+
663 .min-w-fit {671 .min-w-fit {
664 min-width: -moz-fit-content;672 min-width: -moz-fit-content;
665 min-width: fit-content;673 min-width: fit-content;
@@ -717,10 +725,6 @@ video {
717 margin-bottom: calc(1rem * var(--tw-space-y-reverse));725 margin-bottom: calc(1rem * var(--tw-space-y-reverse));
718 }726 }
719 727
720-.rounded {
721- border-radius: 0.25rem;
722-}
723-
724 .rounded-lg {728 .rounded-lg {
725 border-radius: 0.5rem;729 border-radius: 0.5rem;
726 }730 }
@@ -733,6 +737,10 @@ video {
733 border-radius: 0.125rem;737 border-radius: 0.125rem;
734 }738 }
735 739
740+.rounded {
741+ border-radius: 0.25rem;
742+}
743+
736 .border {744 .border {
737 border-width: 1px;745 border-width: 1px;
738 }746 }
@@ -746,6 +754,11 @@ video {
746 border-color: rgb(209 213 219 / var(--tw-border-opacity));754 border-color: rgb(209 213 219 / var(--tw-border-opacity));
747 }755 }
748 756
757+.border-red-400 {
758+ --tw-border-opacity: 1;
759+ border-color: rgb(248 113 113 / var(--tw-border-opacity));
760+}
761+
749 .bg-gordonmascot-hblue {762 .bg-gordonmascot-hblue {
750 --tw-bg-opacity: 1;763 --tw-bg-opacity: 1;
751 background-color: rgb(1 30 92 / var(--tw-bg-opacity));764 background-color: rgb(1 30 92 / var(--tw-bg-opacity));
@@ -761,16 +774,6 @@ video {
761 background-color: rgb(209 213 219 / var(--tw-bg-opacity));774 background-color: rgb(209 213 219 / var(--tw-bg-opacity));
762 }775 }
763 776
764-.bg-green-500 {
765- --tw-bg-opacity: 1;
766- background-color: rgb(34 197 94 / var(--tw-bg-opacity));
767-}
768-
769-.bg-red-500 {
770- --tw-bg-opacity: 1;
771- background-color: rgb(239 68 68 / var(--tw-bg-opacity));
772-}
773-
774 .bg-transparent {777 .bg-transparent {
775 background-color: transparent;778 background-color: transparent;
776 }779 }
@@ -785,6 +788,21 @@ video {
785 background-color: rgb(228 228 231 / var(--tw-bg-opacity));788 background-color: rgb(228 228 231 / var(--tw-bg-opacity));
786 }789 }
787 790
791+.bg-green-500 {
792+ --tw-bg-opacity: 1;
793+ background-color: rgb(34 197 94 / var(--tw-bg-opacity));
794+}
795+
796+.bg-red-500 {
797+ --tw-bg-opacity: 1;
798+ background-color: rgb(239 68 68 / var(--tw-bg-opacity));
799+}
800+
801+.bg-red-100 {
802+ --tw-bg-opacity: 1;
803+ background-color: rgb(254 226 226 / var(--tw-bg-opacity));
804+}
805+
788 .p-0 {806 .p-0 {
789 padding: 0px;807 padding: 0px;
790 }808 }
@@ -807,14 +825,19 @@ video {
807 padding-right: 1rem;825 padding-right: 1rem;
808 }826 }
809 827
828+.py-2 {
829+ padding-top: 0.5rem;
830+ padding-bottom: 0.5rem;
831+}
832+
810 .py-1 {833 .py-1 {
811 padding-top: 0.25rem;834 padding-top: 0.25rem;
812 padding-bottom: 0.25rem;835 padding-bottom: 0.25rem;
813 }836 }
814 837
815-.py-2 {838+.py-3 {
816- padding-top: 0.5rem;839+ padding-top: 0.75rem;
817- padding-bottom: 0.5rem;840+ padding-bottom: 0.75rem;
818 }841 }
819 842
820 .align-baseline {843 .align-baseline {
@@ -846,14 +869,19 @@ video {
846 line-height: 1.25rem;869 line-height: 1.25rem;
847 }870 }
848 871
849-.font-bold {872+.text-xl {
850- font-weight: 700;873+ font-size: 1.25rem;
874+ line-height: 1.75rem;
851 }875 }
852 876
853 .font-semibold {877 .font-semibold {
854 font-weight: 600;878 font-weight: 600;
855 }879 }
856 880
881+.font-bold {
882+ font-weight: 700;
883+}
884+
857 .tracking-tighter {885 .tracking-tighter {
858 letter-spacing: -0.05em;886 letter-spacing: -0.05em;
859 }887 }
@@ -878,9 +906,14 @@ video {
878 color: rgb(31 41 55 / var(--tw-text-opacity));906 color: rgb(31 41 55 / var(--tw-text-opacity));
879 }907 }
880 908
881-.text-red-500 {909+.text-slate-100 {
882 --tw-text-opacity: 1;910 --tw-text-opacity: 1;
883- color: rgb(239 68 68 / var(--tw-text-opacity));911+ color: rgb(241 245 249 / var(--tw-text-opacity));
912+}
913+
914+.text-white {
915+ --tw-text-opacity: 1;
916+ color: rgb(255 255 255 / var(--tw-text-opacity));
884 }917 }
885 918
886 .text-red-800 {919 .text-red-800 {
@@ -888,14 +921,19 @@ video {
888 color: rgb(153 27 27 / var(--tw-text-opacity));921 color: rgb(153 27 27 / var(--tw-text-opacity));
889 }922 }
890 923
891-.text-slate-100 {924+.text-red-700 {
892 --tw-text-opacity: 1;925 --tw-text-opacity: 1;
893- color: rgb(241 245 249 / var(--tw-text-opacity));926+ color: rgb(185 28 28 / var(--tw-text-opacity));
894 }927 }
895 928
896-.text-white {929+.text-red-500 {
897 --tw-text-opacity: 1;930 --tw-text-opacity: 1;
898- color: rgb(255 255 255 / var(--tw-text-opacity));931+ color: rgb(239 68 68 / var(--tw-text-opacity));
932+}
933+
934+.text-red-600 {
935+ --tw-text-opacity: 1;
936+ color: rgb(220 38 38 / var(--tw-text-opacity));
899 }937 }
900 938
901 .underline {939 .underline {
@@ -1033,11 +1071,6 @@ video {
1033 margin-right: 8px;1071 margin-right: 8px;
1034 }1072 }
1035 1073
1036-.hover\:bg-red-700:hover {
1037- --tw-bg-opacity: 1;
1038- background-color: rgb(185 28 28 / var(--tw-bg-opacity));
1039-}
1040-
1041 @media (min-width: 640px) {1074 @media (min-width: 640px) {
1042 .sm\:h-24 {1075 .sm\:h-24 {
1043 height: 6rem;1076 height: 6rem;
M internal/webui/public/assets/js/custom.js +0 -29
@@ -1,29 +0,0 @@
1-// htmx.logAll();
2-document.addEventListener('htmx:afterRequest', function(event) {
3-
4- // Find the error message element
5- var errorMessageElement = document.getElementById('error-message-img');
6-
7- // Find the image div element by ID (you need to get this ID dynamically)
8- var imageDivID = 'img-' + event.detail.elt.id.replace('img-', '');
9- var imageDivElement = document.getElementById(imageDivID);
10- console.log(imageDivElement);
11-
12- // If it's an error, we display and hide the error message
13- if (event.detail.xhr.status !== 200) {
14- if (errorMessageElement && errorMessageElement.textContent.trim() !== "") {
15- // Set a timeout to hide it
16- setTimeout(function() {
17- errorMessageElement.style.display = 'none'; // or set it to empty
18- errorMessageElement.textContent = ''; // clear the text content
19- }, 7000); // 3000 milliseconds = 3 seconds
20- }
21- } else {
22- // If it's a success, we hide the image div
23- if (imageDivElement) {
24- setTimeout(function() {
25- imageDivElement.style.display = 'none'; // Hide the div
26- }, 3000); // 3000 milliseconds = 3 seconds
27- }
28- }
29-});
@@ -1,29 +0,0 @@
1-// htmx.logAll();
2-document.addEventListener('htmx:afterRequest', function(event) {
3-
4- // Find the error message element
5- var errorMessageElement = document.getElementById('error-message-img');
6-
7- // Find the image div element by ID (you need to get this ID dynamically)
8- var imageDivID = 'img-' + event.detail.elt.id.replace('img-', '');
9- var imageDivElement = document.getElementById(imageDivID);
10- console.log(imageDivElement);
11-
12- // If it's an error, we display and hide the error message
13- if (event.detail.xhr.status !== 200) {
14- if (errorMessageElement && errorMessageElement.textContent.trim() !== "") {
15- // Set a timeout to hide it
16- setTimeout(function() {
17- errorMessageElement.style.display = 'none'; // or set it to empty
18- errorMessageElement.textContent = ''; // clear the text content
19- }, 7000); // 3000 milliseconds = 3 seconds
20- }
21- } else {
22- // If it's a success, we hide the image div
23- if (imageDivElement) {
24- setTimeout(function() {
25- imageDivElement.style.display = 'none'; // Hide the div
26- }, 3000); // 3000 milliseconds = 3 seconds
27- }
28- }
29-});
M pkg/utils/docker/client.go +9 -2
@@ -1,7 +1,9 @@
11 package docker
22
33 import (
4+ "context"
45 "fmt"
6+ "log"
57
68 "github.com/docker/docker/client"
79 )
@@ -32,7 +34,8 @@ func (d *DockerClient) InitializeClient(config *Config) error {
3234 client.WithHost("unix://"+config.Sock), // Prepend "unix://" to the Unix socket path
3335 )
3436 if err != nil {
35- return fmt.Errorf("Error from DockerClient: %s", err)
37+ log.Printf("Error initializing Docker client: %s", err)
38+ return err
3639 }
3740 dockerCli = cli
3841 return nil
@@ -42,6 +45,10 @@ func CheckIfInitialized() error {
4245 if dockerCli == nil {
4346 return fmt.Errorf("Docker client is not initialized")
4447 }
45-
48+ // ping the Docker daemon to check if it's running
49+ _, err := dockerCli.Ping(context.Background())
50+ if err != nil {
51+ return fmt.Errorf("Docker client is not initialized")
52+ }
4653 return nil
4754 }
@@ -1,7 +1,9 @@
1 package docker1 package docker
2 2
3 import (3 import (
4+ "context"
4 "fmt"5 "fmt"
6+ "log"
5 7
6 "github.com/docker/docker/client"8 "github.com/docker/docker/client"
7 )9 )
@@ -32,7 +34,8 @@ func (d *DockerClient) InitializeClient(config *Config) error {
32 client.WithHost("unix://"+config.Sock), // Prepend "unix://" to the Unix socket path34 client.WithHost("unix://"+config.Sock), // Prepend "unix://" to the Unix socket path
33 )35 )
34 if err != nil {36 if err != nil {
35- return fmt.Errorf("Error from DockerClient: %s", err)37+ log.Printf("Error initializing Docker client: %s", err)
38+ return err
36 }39 }
37 dockerCli = cli40 dockerCli = cli
38 return nil41 return nil
@@ -42,6 +45,10 @@ func CheckIfInitialized() error {
42 if dockerCli == nil {45 if dockerCli == nil {
43 return fmt.Errorf("Docker client is not initialized")46 return fmt.Errorf("Docker client is not initialized")
44 }47 }
45-48+ // ping the Docker daemon to check if it's running
49+ _, err := dockerCli.Ping(context.Background())
50+ if err != nil {
51+ return fmt.Errorf("Docker client is not initialized")
52+ }
46 return nil53 return nil
47 }54 }
M pkg/utils/docker/containers.go +3 -4
@@ -7,6 +7,9 @@ import (
77 "github.com/docker/docker/api/types"
88 )
99
10+func init() {
11+}
12+
1013 func ListRunningContainers() ([]types.Container, error) {
1114 // Check if the Docker client has been initialized
1215 CheckIfInitialized()
@@ -22,8 +25,6 @@ func ListRunningContainers() ([]types.Container, error) {
2225 // StopContainer attempts to stop a container gracefully with a timeout
2326 // If it doesn't stop, it sets a flag that can be checked to prompt the user.
2427 func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) {
25- // Check if the Docker client has been initialized
26- CheckIfInitialized()
2728
2829 // Start by sending a SIGTERM
2930 err := dockerCli.ContainerKill(context.Background(), containerID, "SIGTERM")
@@ -66,8 +67,6 @@ func StopContainerRagefully(containerID string) error {
6667 }
6768
6869 func DeleteContainer(containerID string) error {
69- // Check if the Docker client has been initialized
70- CheckIfInitialized()
7170
7271 // Delete container using the Docker client
7372 err := dockerCli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{})
@@ -7,6 +7,9 @@ import (
7 "github.com/docker/docker/api/types"7 "github.com/docker/docker/api/types"
8 )8 )
9 9
10+func init() {
11+}
12+
10 func ListRunningContainers() ([]types.Container, error) {13 func ListRunningContainers() ([]types.Container, error) {
11 // Check if the Docker client has been initialized14 // Check if the Docker client has been initialized
12 CheckIfInitialized()15 CheckIfInitialized()
@@ -22,8 +25,6 @@ func ListRunningContainers() ([]types.Container, error) {
22 // StopContainer attempts to stop a container gracefully with a timeout25 // 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.26 // 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) {27 func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) {
25- // Check if the Docker client has been initialized
26- CheckIfInitialized()
27 28
28 // Start by sending a SIGTERM29 // Start by sending a SIGTERM
29 err := dockerCli.ContainerKill(context.Background(), containerID, "SIGTERM")30 err := dockerCli.ContainerKill(context.Background(), containerID, "SIGTERM")
@@ -66,8 +67,6 @@ func StopContainerRagefully(containerID string) error {
66 }67 }
67 68
68 func DeleteContainer(containerID string) error {69 func DeleteContainer(containerID string) error {
69- // Check if the Docker client has been initialized
70- CheckIfInitialized()
71 70
72 // Delete container using the Docker client71 // Delete container using the Docker client
73 err := dockerCli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{})72 err := dockerCli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{})
M pkg/utils/docker/images.go +0 -1
@@ -20,7 +20,6 @@ func ListContainerImages() ([]types.ImageSummary, error) {
2020 }
2121 func DeleteContainerImage(imageID string) error {
2222 // Check if the Docker client has been initialized
23- CheckIfInitialized()
2423
2524 // Delete the image using the Docker client
2625 _, err := dockerCli.ImageRemove(context.Background(), imageID, types.ImageRemoveOptions{})
@@ -20,7 +20,6 @@ func ListContainerImages() ([]types.ImageSummary, error) {
20 }20 }
21 func DeleteContainerImage(imageID string) error {21 func DeleteContainerImage(imageID string) error {
22 // Check if the Docker client has been initialized22 // Check if the Docker client has been initialized
23- CheckIfInitialized()
24 23
25 // Delete the image using the Docker client24 // Delete the image using the Docker client
26 _, err := dockerCli.ImageRemove(context.Background(), imageID, types.ImageRemoveOptions{})25 _, err := dockerCli.ImageRemove(context.Background(), imageID, types.ImageRemoveOptions{})

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