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