detached 90623ac

feat: Implement Edit Container component

The new component displays container information in a textarea. The information is presented in YAML-formatted syntax for easier editing and understanding.
commit
90623aced9bbc97aa0c12712854d8d81a53a9080
author
bnema · · 2023-09-27 11:31 UTC
parent
0fbbe4d
signature
unsigned
8 files · +272 -15
M internal/gotemplate/models/html/fragments/containerlist.gohtml +17 -0
@@ -51,6 +51,18 @@
5151 >
5252 <span class="iconoir-system-shut text-xl font-bold"></span>
5353 </button>
54+ <!-- EDIT -->
55+ <button
56+ id="edit-button-{{.ID}}"
57+ hx-get="/htmx/container-manager/edit/{{.ID}}"
58+ hx-target="#edit-container-{{.ID}}"
59+ hx-target-*="#error-message-{{.ID}}"
60+ hx-swap="innerHTML"
61+ class="py-2 px-4"
62+ data-active="false"
63+ >
64+ <span class="iconoir-edit text-xl text-blue-600 font-bold"></span>
65+ </button>
5466 <!-- REMOVE -->
5567 <button
5668 id="remove-button-img-{{.ID}}"
@@ -66,6 +78,11 @@
6678 </button>
6779 </td>
6880 </tr>
81+ <tr>
82+ <td colspan="7">
83+ <div id="edit-container-{{.ID}}"></div>
84+ </td>
85+ </tr>
6986 <tr>
7087 <td colspan="7">
7188 <div id="error-message-{{.ID}}" class="text-red-500"></div>
@@ -51,6 +51,18 @@
51 >51 >
52 <span class="iconoir-system-shut text-xl font-bold"></span>52 <span class="iconoir-system-shut text-xl font-bold"></span>
53 </button>53 </button>
54+ <!-- EDIT -->
55+ <button
56+ id="edit-button-{{.ID}}"
57+ hx-get="/htmx/container-manager/edit/{{.ID}}"
58+ hx-target="#edit-container-{{.ID}}"
59+ hx-target-*="#error-message-{{.ID}}"
60+ hx-swap="innerHTML"
61+ class="py-2 px-4"
62+ data-active="false"
63+ >
64+ <span class="iconoir-edit text-xl text-blue-600 font-bold"></span>
65+ </button>
54 <!-- REMOVE -->66 <!-- REMOVE -->
55 <button67 <button
56 id="remove-button-img-{{.ID}}"68 id="remove-button-img-{{.ID}}"
@@ -66,6 +78,11 @@
66 </button>78 </button>
67 </td>79 </td>
68 </tr>80 </tr>
81+ <tr>
82+ <td colspan="7">
83+ <div id="edit-container-{{.ID}}"></div>
84+ </td>
85+ </tr>
69 <tr>86 <tr>
70 <td colspan="7">87 <td colspan="7">
71 <div id="error-message-{{.ID}}" class="text-red-500"></div>88 <div id="error-message-{{.ID}}" class="text-red-500"></div>
A internal/gotemplate/models/html/fragments/editcontainer.gohtml +17 -0
@@ -0,0 +1,17 @@
1+<div id="editor">
2+ <form method="post" action="/htmx/container-manager/edit/{{.ID}}">
3+ <textarea
4+ name="container_config"
5+ id="container_config"
6+ class="w-full p-4 rounded-md bg-slate-100 border-gray-300 border-2"
7+ oninput="this.style.height = '';this.style.height = this.scrollHeight + 'px'"
8+ >{{.ContainerInfo}}</textarea
9+ >
10+
11+ <input
12+ type="submit"
13+ value="Save"
14+ class="bg-gordonmascot-mblue text-white px-4 py-2 rounded-md md:text-xl hover:bg-gordonmascot-hblue"
15+ />
16+ </form>
17+</div>
@@ -0,0 +1,17 @@
1+<div id="editor">
2+ <form method="post" action="/htmx/container-manager/edit/{{.ID}}">
3+ <textarea
4+ name="container_config"
5+ id="container_config"
6+ class="w-full p-4 rounded-md bg-slate-100 border-gray-300 border-2"
7+ oninput="this.style.height = '';this.style.height = this.scrollHeight + 'px'"
8+ >{{.ContainerInfo}}</textarea
9+ >
10+
11+ <input
12+ type="submit"
13+ value="Save"
14+ class="bg-gordonmascot-mblue text-white px-4 py-2 rounded-md md:text-xl hover:bg-gordonmascot-hblue"
15+ />
16+ </form>
17+</div>
M internal/httpserve/handler/createcontainer.go +1 -15
@@ -10,18 +10,6 @@ import (
1010 "github.com/labstack/echo/v4"
1111 )
1212
13-type CreateContainer struct {
14- ContainerName string
15- ContainerHost string
16- Environment string
17- ImageName string
18- ImageID string
19- Ports string
20- Volumes string
21- TraefikLabels []string
22- Network string
23-}
24-
2513 // FromShortIDToImageID converts a short image ID to a full image ID
2614 func FromShortIDToImageID(ShortID string) (string, error) {
2715 idMapMutex.Lock()
@@ -118,9 +106,7 @@ func CreateContainerPOST(c echo.Context, a *app.App) error {
118106 }
119107 }
120108
121- // Result : Form values: map[container_domain:gogs.localhost container_name:gogs image_id:sha256:5d92d8ae733d93dc4af5b32bb353539eeff660fc2136c6902abcdf3aafe4cef1 image_name:gogs/gogs:latest ports:8888:80 restart:unless-stopped volumes:/home/data/test:/data]
122- // With those values we can use gotemplate/txt to create a custom docker run command
123- cmdParams, err := render.FromInputsToCmdParams(sanitizedInputs) // Make sure cmdParams is of type render.ContainerCommandParams
109+ cmdParams, err := render.FromInputsToCmdParams(sanitizedInputs)
124110 if err != nil {
125111 return sendError(c, err)
126112 }
@@ -10,18 +10,6 @@ import (
10 "github.com/labstack/echo/v4"10 "github.com/labstack/echo/v4"
11 )11 )
12 12
13-type CreateContainer struct {
14- ContainerName string
15- ContainerHost string
16- Environment string
17- ImageName string
18- ImageID string
19- Ports string
20- Volumes string
21- TraefikLabels []string
22- Network string
23-}
24-
25 // FromShortIDToImageID converts a short image ID to a full image ID13 // FromShortIDToImageID converts a short image ID to a full image ID
26 func FromShortIDToImageID(ShortID string) (string, error) {14 func FromShortIDToImageID(ShortID string) (string, error) {
27 idMapMutex.Lock()15 idMapMutex.Lock()
@@ -118,9 +106,7 @@ func CreateContainerPOST(c echo.Context, a *app.App) error {
118 }106 }
119 }107 }
120 108
121- // Result : Form values: map[container_domain:gogs.localhost container_name:gogs image_id:sha256:5d92d8ae733d93dc4af5b32bb353539eeff660fc2136c6902abcdf3aafe4cef1 image_name:gogs/gogs:latest ports:8888:80 restart:unless-stopped volumes:/home/data/test:/data]109+ cmdParams, err := render.FromInputsToCmdParams(sanitizedInputs)
122- // With those values we can use gotemplate/txt to create a custom docker run command
123- cmdParams, err := render.FromInputsToCmdParams(sanitizedInputs) // Make sure cmdParams is of type render.ContainerCommandParams
124 if err != nil {110 if err != nil {
125 return sendError(c, err)111 return sendError(c, err)
126 }112 }
A internal/httpserve/handler/editcontainer.go +122 -0
@@ -0,0 +1,122 @@
1+package handler
2+
3+import (
4+ "fmt"
5+ "strings"
6+
7+ "github.com/bnema/gordon/internal/app"
8+ "github.com/bnema/gordon/internal/gotemplate/render"
9+ "github.com/bnema/gordon/pkg/utils/docker"
10+ "github.com/labstack/echo/v4"
11+ "gopkg.in/yaml.v3"
12+)
13+
14+func generateOrderedYAML(infoMap map[string]interface{}) (string, error) {
15+ orderedKeys := []string{"Name", "Image", "Hostname", "Ports", "Environment", "Labels", "Network", "Volumes", "Restart"}
16+
17+ var yamlString string
18+ for _, key := range orderedKeys {
19+ value, exists := infoMap[key]
20+ if !exists {
21+ continue
22+ }
23+
24+ yamlBytes, err := yaml.Marshal(map[string]interface{}{key: value})
25+ if err != nil {
26+ return "", err
27+ }
28+
29+ yamlString += string(yamlBytes)
30+ }
31+
32+ return yamlString, nil
33+}
34+
35+// ContainerManagerEditGET displays the edit container view
36+func ContainerManagerEditGET(c echo.Context, a *app.App) error {
37+ containerID := c.Param("ID")
38+
39+ fmt.Println(containerID)
40+
41+ // Get the container info
42+ containerInfo, err := docker.GetContainerInfo(containerID)
43+ if err != nil {
44+ return sendError(c, err)
45+ }
46+
47+ // Trim the slash from the container name
48+ containerInfo.Name = strings.TrimPrefix(containerInfo.Name, "/")
49+ // Get just the network names
50+ networkNames := make([]string, 0, len(containerInfo.NetworkSettings.Networks))
51+ for networkName := range containerInfo.NetworkSettings.Networks {
52+ networkNames = append(networkNames, networkName)
53+ }
54+
55+ // Prepare Ports
56+ portMappings := make([]string, 0)
57+ for port, binding := range containerInfo.NetworkSettings.Ports {
58+ for _, b := range binding {
59+ portMappings = append(portMappings, fmt.Sprintf("%s:%s->%s", b.HostIP, b.HostPort, port))
60+ }
61+ }
62+
63+ // Prepare Volumes (Mounts)
64+ volumeMappings := make([]string, 0)
65+ for _, mount := range containerInfo.Mounts {
66+ volumeMappings = append(volumeMappings, fmt.Sprintf("%s:%s", mount.Source, mount.Destination))
67+ }
68+
69+ // Initialize a map to store the information you want to display
70+ infoMap := make(map[string]interface{})
71+
72+ // Populate the map with container details
73+ infoMap["Name"] = containerInfo.Name
74+ infoMap["Image"] = containerInfo.Config.Image
75+ infoMap["Hostname"] = containerInfo.Config.Hostname
76+ infoMap["Ports"] = portMappings
77+ infoMap["Volumes"] = volumeMappings
78+ infoMap["Environment"] = containerInfo.Config.Env
79+ infoMap["Labels"] = containerInfo.Config.Labels
80+ infoMap["Network"] = networkNames
81+ infoMap["Restart"] = containerInfo.HostConfig.RestartPolicy.Name
82+
83+ // Convert the infoMap to YAML format
84+ yamlString, err := generateOrderedYAML(infoMap)
85+ if err != nil {
86+ return sendError(c, err)
87+ }
88+
89+ // Pass the containerInfo to the data map
90+ data := map[string]interface{}{
91+ "Title": "Edit container",
92+ "ContainerInfo": yamlString,
93+ }
94+
95+ rendererData, err := render.GetHTMLRenderer("html/fragments", "editcontainer.gohtml", a.TemplateFS, a)
96+
97+ if err != nil {
98+ return sendError(c, err)
99+ }
100+
101+ renderedHTML, err := rendererData.Render(data, a)
102+
103+ if err != nil {
104+ return sendError(c, err)
105+ }
106+
107+ return c.HTML(200, renderedHTML)
108+}
109+
110+// ContainerManagerEditPOST is the route action for editing a container
111+func ContainerManagerEditPOST(c echo.Context, a *app.App) error {
112+ containerID := c.Param("ID")
113+ fmt.Println(containerID)
114+
115+ // Get the new config
116+ newConfig := c.FormValue("container_config")
117+
118+ fmt.Println(newConfig)
119+
120+ return nil
121+
122+}
@@ -0,0 +1,122 @@
1+package handler
2+
3+import (
4+ "fmt"
5+ "strings"
6+
7+ "github.com/bnema/gordon/internal/app"
8+ "github.com/bnema/gordon/internal/gotemplate/render"
9+ "github.com/bnema/gordon/pkg/utils/docker"
10+ "github.com/labstack/echo/v4"
11+ "gopkg.in/yaml.v3"
12+)
13+
14+func generateOrderedYAML(infoMap map[string]interface{}) (string, error) {
15+ orderedKeys := []string{"Name", "Image", "Hostname", "Ports", "Environment", "Labels", "Network", "Volumes", "Restart"}
16+
17+ var yamlString string
18+ for _, key := range orderedKeys {
19+ value, exists := infoMap[key]
20+ if !exists {
21+ continue
22+ }
23+
24+ yamlBytes, err := yaml.Marshal(map[string]interface{}{key: value})
25+ if err != nil {
26+ return "", err
27+ }
28+
29+ yamlString += string(yamlBytes)
30+ }
31+
32+ return yamlString, nil
33+}
34+
35+// ContainerManagerEditGET displays the edit container view
36+func ContainerManagerEditGET(c echo.Context, a *app.App) error {
37+ containerID := c.Param("ID")
38+
39+ fmt.Println(containerID)
40+
41+ // Get the container info
42+ containerInfo, err := docker.GetContainerInfo(containerID)
43+ if err != nil {
44+ return sendError(c, err)
45+ }
46+
47+ // Trim the slash from the container name
48+ containerInfo.Name = strings.TrimPrefix(containerInfo.Name, "/")
49+ // Get just the network names
50+ networkNames := make([]string, 0, len(containerInfo.NetworkSettings.Networks))
51+ for networkName := range containerInfo.NetworkSettings.Networks {
52+ networkNames = append(networkNames, networkName)
53+ }
54+
55+ // Prepare Ports
56+ portMappings := make([]string, 0)
57+ for port, binding := range containerInfo.NetworkSettings.Ports {
58+ for _, b := range binding {
59+ portMappings = append(portMappings, fmt.Sprintf("%s:%s->%s", b.HostIP, b.HostPort, port))
60+ }
61+ }
62+
63+ // Prepare Volumes (Mounts)
64+ volumeMappings := make([]string, 0)
65+ for _, mount := range containerInfo.Mounts {
66+ volumeMappings = append(volumeMappings, fmt.Sprintf("%s:%s", mount.Source, mount.Destination))
67+ }
68+
69+ // Initialize a map to store the information you want to display
70+ infoMap := make(map[string]interface{})
71+
72+ // Populate the map with container details
73+ infoMap["Name"] = containerInfo.Name
74+ infoMap["Image"] = containerInfo.Config.Image
75+ infoMap["Hostname"] = containerInfo.Config.Hostname
76+ infoMap["Ports"] = portMappings
77+ infoMap["Volumes"] = volumeMappings
78+ infoMap["Environment"] = containerInfo.Config.Env
79+ infoMap["Labels"] = containerInfo.Config.Labels
80+ infoMap["Network"] = networkNames
81+ infoMap["Restart"] = containerInfo.HostConfig.RestartPolicy.Name
82+
83+ // Convert the infoMap to YAML format
84+ yamlString, err := generateOrderedYAML(infoMap)
85+ if err != nil {
86+ return sendError(c, err)
87+ }
88+
89+ // Pass the containerInfo to the data map
90+ data := map[string]interface{}{
91+ "Title": "Edit container",
92+ "ContainerInfo": yamlString,
93+ }
94+
95+ rendererData, err := render.GetHTMLRenderer("html/fragments", "editcontainer.gohtml", a.TemplateFS, a)
96+
97+ if err != nil {
98+ return sendError(c, err)
99+ }
100+
101+ renderedHTML, err := rendererData.Render(data, a)
102+
103+ if err != nil {
104+ return sendError(c, err)
105+ }
106+
107+ return c.HTML(200, renderedHTML)
108+}
109+
110+// ContainerManagerEditPOST is the route action for editing a container
111+func ContainerManagerEditPOST(c echo.Context, a *app.App) error {
112+ containerID := c.Param("ID")
113+ fmt.Println(containerID)
114+
115+ // Get the new config
116+ newConfig := c.FormValue("container_config")
117+
118+ fmt.Println(newConfig)
119+
120+ return nil
121+
122+}
M internal/httpserve/router.go +8 -0
@@ -92,6 +92,14 @@ func bindHTMXEndpoints(e *echo.Echo, a *app.App) {
9292 e.POST("/htmx/container-manager/start/:ID", func(c echo.Context) error {
9393 return handler.ContainerManagerStart(c, a)
9494 })
95+ // Edit a container view
96+ e.GET("/htmx/container-manager/edit/:ID", func(c echo.Context) error {
97+ return handler.ContainerManagerEditGET(c, a)
98+ })
99+ // Edit a container action
100+ e.POST("/htmx/container-manager/edit/:ID", func(c echo.Context) error {
101+ return handler.ContainerManagerEditPOST(c, a)
102+ })
95103
96104 // Display upload-image component
97105 e.GET("/htmx/upload-image", func(c echo.Context) error {
@@ -92,6 +92,14 @@ func bindHTMXEndpoints(e *echo.Echo, a *app.App) {
92 e.POST("/htmx/container-manager/start/:ID", func(c echo.Context) error {92 e.POST("/htmx/container-manager/start/:ID", func(c echo.Context) error {
93 return handler.ContainerManagerStart(c, a)93 return handler.ContainerManagerStart(c, a)
94 })94 })
95+ // Edit a container view
96+ e.GET("/htmx/container-manager/edit/:ID", func(c echo.Context) error {
97+ return handler.ContainerManagerEditGET(c, a)
98+ })
99+ // Edit a container action
100+ e.POST("/htmx/container-manager/edit/:ID", func(c echo.Context) error {
101+ return handler.ContainerManagerEditPOST(c, a)
102+ })
95 103
96 // Display upload-image component104 // Display upload-image component
97 e.GET("/htmx/upload-image", func(c echo.Context) error {105 e.GET("/htmx/upload-image", func(c echo.Context) error {
M internal/webui/public/assets/css/tailwind.css +25 -0
@@ -822,6 +822,11 @@ video {
822822 background-color: rgb(228 228 231 / var(--tw-bg-opacity));
823823 }
824824
825+.bg-gordonmascot-mblue {
826+ --tw-bg-opacity: 1;
827+ background-color: rgb(0 82 147 / var(--tw-bg-opacity));
828+}
829+
825830 .p-0 {
826831 padding: 0px;
827832 }
@@ -854,6 +859,11 @@ video {
854859 padding-bottom: 0.5rem;
855860 }
856861
862+.py-4 {
863+ padding-top: 1rem;
864+ padding-bottom: 1rem;
865+}
866+
857867 .pb-4 {
858868 padding-bottom: 1rem;
859869 }
@@ -959,6 +969,16 @@ video {
959969 color: rgb(255 255 255 / var(--tw-text-opacity));
960970 }
961971
972+.text-blue-600 {
973+ --tw-text-opacity: 1;
974+ color: rgb(37 99 235 / var(--tw-text-opacity));
975+}
976+
977+.text-black {
978+ --tw-text-opacity: 1;
979+ color: rgb(0 0 0 / var(--tw-text-opacity));
980+}
981+
962982 .underline {
963983 text-decoration-line: underline;
964984 }
@@ -1099,6 +1119,11 @@ video {
10991119 background-color: rgb(29 78 216 / var(--tw-bg-opacity));
11001120 }
11011121
1122+.hover\:bg-gordonmascot-hblue:hover {
1123+ --tw-bg-opacity: 1;
1124+ background-color: rgb(1 30 92 / var(--tw-bg-opacity));
1125+}
1126+
11021127 @media (min-width: 640px) {
11031128 .sm\:h-24 {
11041129 height: 6rem;
@@ -822,6 +822,11 @@ video {
822 background-color: rgb(228 228 231 / var(--tw-bg-opacity));822 background-color: rgb(228 228 231 / var(--tw-bg-opacity));
823 }823 }
824 824
825+.bg-gordonmascot-mblue {
826+ --tw-bg-opacity: 1;
827+ background-color: rgb(0 82 147 / var(--tw-bg-opacity));
828+}
829+
825 .p-0 {830 .p-0 {
826 padding: 0px;831 padding: 0px;
827 }832 }
@@ -854,6 +859,11 @@ video {
854 padding-bottom: 0.5rem;859 padding-bottom: 0.5rem;
855 }860 }
856 861
862+.py-4 {
863+ padding-top: 1rem;
864+ padding-bottom: 1rem;
865+}
866+
857 .pb-4 {867 .pb-4 {
858 padding-bottom: 1rem;868 padding-bottom: 1rem;
859 }869 }
@@ -959,6 +969,16 @@ video {
959 color: rgb(255 255 255 / var(--tw-text-opacity));969 color: rgb(255 255 255 / var(--tw-text-opacity));
960 }970 }
961 971
972+.text-blue-600 {
973+ --tw-text-opacity: 1;
974+ color: rgb(37 99 235 / var(--tw-text-opacity));
975+}
976+
977+.text-black {
978+ --tw-text-opacity: 1;
979+ color: rgb(0 0 0 / var(--tw-text-opacity));
980+}
981+
962 .underline {982 .underline {
963 text-decoration-line: underline;983 text-decoration-line: underline;
964 }984 }
@@ -1099,6 +1119,11 @@ video {
1099 background-color: rgb(29 78 216 / var(--tw-bg-opacity));1119 background-color: rgb(29 78 216 / var(--tw-bg-opacity));
1100 }1120 }
1101 1121
1122+.hover\:bg-gordonmascot-hblue:hover {
1123+ --tw-bg-opacity: 1;
1124+ background-color: rgb(1 30 92 / var(--tw-bg-opacity));
1125+}
1126+
1102 @media (min-width: 640px) {1127 @media (min-width: 640px) {
1103 .sm\:h-24 {1128 .sm\:h-24 {
1104 height: 6rem;1129 height: 6rem;
M internal/webui/public/assets/js/custom.js +47 -0
@@ -9,3 +9,50 @@ document.body.addEventListener("htmx:afterOnLoad", function(event) {
99 }
1010 });
1111
12+function toggleButtonState(event) {
13+ const button = event.currentTarget;
14+ const isActive = button.getAttribute('data-active') === 'true';
15+
16+ // Toggle the state
17+ button.setAttribute('data-active', !isActive);
18+
19+ // Get the related component based on hx-target attribute
20+ const hxTarget = button.getAttribute('hx-target');
21+ const relatedComponent = document.querySelector(hxTarget);
22+
23+ if (relatedComponent) {
24+ if (isActive) {
25+ relatedComponent.style.display = 'none'; // Hide
26+ } else {
27+ relatedComponent.style.display = 'block'; // Show
28+
29+ // Adjust textarea height if it exists within the relatedComponent
30+ const textarea = relatedComponent.querySelector("#container_config");
31+ if (textarea) {
32+ textarea.style.height = ""; // Reset the height
33+ textarea.style.height = textarea.scrollHeight + "px";
34+ }
35+ }
36+ }
37+}
38+
39+
40+// Attach event listeners to buttons
41+document.body.addEventListener("htmx:afterSwap", function (event) {
42+ const buttons = document.querySelectorAll('[id^="start-button-"], [id^="stop-button-"], [id^="edit-button-"], [id^="remove-button-img-"]');
43+ buttons.forEach(button => {
44+ console.log("button: ", button);
45+ button.addEventListener('click', toggleButtonState);
46+ });
47+});
48+
49+ document.body.addEventListener("htmx:afterSwap", function (event) {
50+ const textarea = document.getElementById("container_config");
51+ if (textarea) {
52+ console.log("Textarea found, adjusting height.");
53+ textarea.style.height = ""; // Reset the height
54+ textarea.style.height = textarea.scrollHeight + "px";
55+ } else {
56+ console.log("Textarea not found.");
57+ }
58+ });⏎̸
@@ -9,3 +9,50 @@ document.body.addEventListener("htmx:afterOnLoad", function(event) {
9 }9 }
10 });10 });
11 11
12+function toggleButtonState(event) {
13+ const button = event.currentTarget;
14+ const isActive = button.getAttribute('data-active') === 'true';
15+
16+ // Toggle the state
17+ button.setAttribute('data-active', !isActive);
18+
19+ // Get the related component based on hx-target attribute
20+ const hxTarget = button.getAttribute('hx-target');
21+ const relatedComponent = document.querySelector(hxTarget);
22+
23+ if (relatedComponent) {
24+ if (isActive) {
25+ relatedComponent.style.display = 'none'; // Hide
26+ } else {
27+ relatedComponent.style.display = 'block'; // Show
28+
29+ // Adjust textarea height if it exists within the relatedComponent
30+ const textarea = relatedComponent.querySelector("#container_config");
31+ if (textarea) {
32+ textarea.style.height = ""; // Reset the height
33+ textarea.style.height = textarea.scrollHeight + "px";
34+ }
35+ }
36+ }
37+}
38+
39+
40+// Attach event listeners to buttons
41+document.body.addEventListener("htmx:afterSwap", function (event) {
42+ const buttons = document.querySelectorAll('[id^="start-button-"], [id^="stop-button-"], [id^="edit-button-"], [id^="remove-button-img-"]');
43+ buttons.forEach(button => {
44+ console.log("button: ", button);
45+ button.addEventListener('click', toggleButtonState);
46+ });
47+});
48+
49+ document.body.addEventListener("htmx:afterSwap", function (event) {
50+ const textarea = document.getElementById("container_config");
51+ if (textarea) {
52+ console.log("Textarea found, adjusting height.");
53+ textarea.style.height = ""; // Reset the height
54+ textarea.style.height = textarea.scrollHeight + "px";
55+ } else {
56+ console.log("Textarea not found.");
57+ }
58+ });⏎̸
M pkg/utils/docker/containers.go +35 -0
@@ -182,6 +182,41 @@ func GetContainerInfo(containerID string) (types.ContainerJSON, error) {
182182 return containerInfo, nil
183183 }
184184
185+// UpdateContainerConfig updates the configuration of an existing container.
186+func UpdateContainerConfig(containerID string, newConfig *container.Config) error {
187+ ctx := context.Background()
188+ // 1. Gracefully stop the existing container
189+ _, err := StopContainerGracefully(containerID, 3*time.Second)
190+ if err != nil {
191+ return err
192+ }
193+
194+ // 2. Remove the old container
195+ if err := dockerCli.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{}); err != nil {
196+ return err
197+ }
198+
199+ // 3. Create a new container with the new configuration
200+ resp, err := dockerCli.ContainerCreate(
201+ ctx,
202+ newConfig,
203+ &container.HostConfig{},
204+ &network.NetworkingConfig{},
205+ nil,
206+ "",
207+ )
208+ if err != nil {
209+ return err
210+ }
211+
212+ // 4. Start the new container
213+ if err := dockerCli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
214+ return err
215+ }
216+
217+ return nil
218+}
219+
185220 // GetContainerLogs returns the logs of a container
186221 func GetContainerLogs(containerID string) (string, error) {
187222
@@ -182,6 +182,41 @@ func GetContainerInfo(containerID string) (types.ContainerJSON, error) {
182 return containerInfo, nil182 return containerInfo, nil
183 }183 }
184 184
185+// UpdateContainerConfig updates the configuration of an existing container.
186+func UpdateContainerConfig(containerID string, newConfig *container.Config) error {
187+ ctx := context.Background()
188+ // 1. Gracefully stop the existing container
189+ _, err := StopContainerGracefully(containerID, 3*time.Second)
190+ if err != nil {
191+ return err
192+ }
193+
194+ // 2. Remove the old container
195+ if err := dockerCli.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{}); err != nil {
196+ return err
197+ }
198+
199+ // 3. Create a new container with the new configuration
200+ resp, err := dockerCli.ContainerCreate(
201+ ctx,
202+ newConfig,
203+ &container.HostConfig{},
204+ &network.NetworkingConfig{},
205+ nil,
206+ "",
207+ )
208+ if err != nil {
209+ return err
210+ }
211+
212+ // 4. Start the new container
213+ if err := dockerCli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
214+ return err
215+ }
216+
217+ return nil
218+}
219+
185 // GetContainerLogs returns the logs of a container220 // GetContainerLogs returns the logs of a container
186 func GetContainerLogs(containerID string) (string, error) {221 func GetContainerLogs(containerID string) (string, error) {
187 222

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