Github App integration complete, Also add a a basic manager to interact with docker
- commit
- 864f556aa0a400f263194be0c72cc1f7a6f27f08
- author
- bnema · · 2023-09-20 18:52 UTC
- committer
- bnema · · 2023-09-20 21:58 UTC
- parent
- 1a422e8
- signature
- unsigned
14 files · +317 -106
M cmd/main.go +6 -0
| @@ -12,6 +12,7 @@ import ( | |||
| 12 | 12 | ||
| 13 | 13 | "github.com/bnema/gordon/internal/app" | |
| 14 | 14 | "github.com/bnema/gordon/internal/httpserve" | |
| 15 | + | "github.com/bnema/gordon/pkg/utils/docker" | |
| 15 | 16 | ) | |
| 16 | 17 | ||
| 17 | 18 | func cleanup(a *app.App, memDb *sql.DB) { | |
| @@ -31,6 +32,11 @@ func main() { | |||
| 31 | 32 | // Pass memDb to the app with the rest of the configs | |
| 32 | 33 | a.DB = memDb | |
| 33 | 34 | ||
| 35 | + | dockerClient := &docker.DockerClient{} | |
| 36 | + | err = dockerClient.InitializeClient(a.Config.NewDockerConfig()) | |
| 37 | + | if err != nil { | |
| 38 | + | log.Fatalf("Error initializing Docker client: %s", err) | |
| 39 | + | } | |
| 34 | 40 | // Setup a channel to capture termination signals | |
| 35 | 41 | sigs := make(chan os.Signal, 1) | |
| 36 | 42 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) | |
| @@ -12,6 +12,7 @@ import ( | |||||
| 12 | 12 | ||||
| 13 | "github.com/bnema/gordon/internal/app" | 13 | "github.com/bnema/gordon/internal/app" | ||
| 14 | "github.com/bnema/gordon/internal/httpserve" | 14 | "github.com/bnema/gordon/internal/httpserve" | ||
| 15 | + | "github.com/bnema/gordon/pkg/utils/docker" | |||
| 15 | ) | 16 | ) | ||
| 16 | 17 | ||||
| 17 | func cleanup(a *app.App, memDb *sql.DB) { | 18 | func cleanup(a *app.App, memDb *sql.DB) { | ||
| @@ -31,6 +32,11 @@ func main() { | |||||
| 31 | // Pass memDb to the app with the rest of the configs | 32 | // Pass memDb to the app with the rest of the configs | ||
| 32 | a.DB = memDb | 33 | a.DB = memDb | ||
| 33 | 34 | ||||
| 35 | + | dockerClient := &docker.DockerClient{} | |||
| 36 | + | err = dockerClient.InitializeClient(a.Config.NewDockerConfig()) | |||
| 37 | + | if err != nil { | |||
| 38 | + | log.Fatalf("Error initializing Docker client: %s", err) | |||
| 39 | + | } | |||
| 34 | // Setup a channel to capture termination signals | 40 | // Setup a channel to capture termination signals | ||
| 35 | sigs := make(chan os.Signal, 1) | 41 | sigs := make(chan os.Signal, 1) | ||
| 36 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) | 42 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) | ||
M internal/app/app.go +5 -4
| @@ -16,13 +16,14 @@ import ( | |||
| 16 | 16 | var ( | |
| 17 | 17 | OauthCallbackURL string | |
| 18 | 18 | appEnv string | |
| 19 | + | BuildVersion = "0.0.2" | |
| 19 | 20 | ) | |
| 20 | 21 | ||
| 21 | 22 | func init() { | |
| 22 | 23 | appEnv = os.Getenv("APP_ENV") | |
| 23 | 24 | if appEnv == "" { | |
| 24 | 25 | appEnv = "prod" // Default to "prod" if APP_ENV is not set | |
| 25 | - | } | |
| 26 | + | } | |
| 26 | 27 | } | |
| 27 | 28 | ||
| 28 | 29 | type App struct { | |
| @@ -44,7 +45,7 @@ type AppConfig struct { | |||
| 44 | 45 | ||
| 45 | 46 | type GeneralConfig struct { | |
| 46 | 47 | RunEnv string `yaml:"runEnv"` | |
| 47 | - | BuildVersion string `yaml:"buildVersion"` | |
| 48 | + | BuildVersion string | |
| 48 | 49 | } | |
| 49 | 50 | ||
| 50 | 51 | type HttpConfig struct { | |
| @@ -68,13 +69,13 @@ func InitializeEnvironment() { | |||
| 68 | 69 | fmt.Errorf("Error initializing environment: %s", err) | |
| 69 | 70 | } | |
| 70 | 71 | OauthCallbackURL = GenerateOauthCallbackURL(config) | |
| 71 | - | ||
| 72 | 72 | } | |
| 73 | 73 | ||
| 74 | 74 | func LoadConfig() (AppConfig, error) { | |
| 75 | 75 | var config AppConfig | |
| 76 | 76 | configDir, configFile := getConfigFile() | |
| 77 | - | fsys := os.DirFS(configDir) // Use directory path here | |
| 77 | + | fsys := os.DirFS(configDir) | |
| 78 | + | BuildVersion = config.General.BuildVersion // Use directory path here | |
| 78 | 79 | err := parser.OpenYamlFile(fsys, configFile, &config) // Assuming it doesn't need the last argument | |
| 79 | 80 | return config, err | |
| 80 | 81 | } | |
| @@ -16,13 +16,14 @@ import ( | |||||
| 16 | var ( | 16 | var ( | ||
| 17 | OauthCallbackURL string | 17 | OauthCallbackURL string | ||
| 18 | appEnv string | 18 | appEnv string | ||
| 19 | + | BuildVersion = "0.0.2" | |||
| 19 | ) | 20 | ) | ||
| 20 | 21 | ||||
| 21 | func init() { | 22 | func init() { | ||
| 22 | appEnv = os.Getenv("APP_ENV") | 23 | appEnv = os.Getenv("APP_ENV") | ||
| 23 | if appEnv == "" { | 24 | if appEnv == "" { | ||
| 24 | appEnv = "prod" // Default to "prod" if APP_ENV is not set | 25 | appEnv = "prod" // Default to "prod" if APP_ENV is not set | ||
| 25 | - | } | 26 | + | } |
| 26 | } | 27 | } | ||
| 27 | 28 | ||||
| 28 | type App struct { | 29 | type App struct { | ||
| @@ -44,7 +45,7 @@ type AppConfig struct { | |||||
| 44 | 45 | ||||
| 45 | type GeneralConfig struct { | 46 | type GeneralConfig struct { | ||
| 46 | RunEnv string `yaml:"runEnv"` | 47 | RunEnv string `yaml:"runEnv"` | ||
| 47 | - | BuildVersion string `yaml:"buildVersion"` | 48 | + | BuildVersion string |
| 48 | } | 49 | } | ||
| 49 | 50 | ||||
| 50 | type HttpConfig struct { | 51 | type HttpConfig struct { | ||
| @@ -68,13 +69,13 @@ func InitializeEnvironment() { | |||||
| 68 | fmt.Errorf("Error initializing environment: %s", err) | 69 | fmt.Errorf("Error initializing environment: %s", err) | ||
| 69 | } | 70 | } | ||
| 70 | OauthCallbackURL = GenerateOauthCallbackURL(config) | 71 | OauthCallbackURL = GenerateOauthCallbackURL(config) | ||
| 71 | - | ||||
| 72 | } | 72 | } | ||
| 73 | 73 | ||||
| 74 | func LoadConfig() (AppConfig, error) { | 74 | func LoadConfig() (AppConfig, error) { | ||
| 75 | var config AppConfig | 75 | var config AppConfig | ||
| 76 | configDir, configFile := getConfigFile() | 76 | configDir, configFile := getConfigFile() | ||
| 77 | - | fsys := os.DirFS(configDir) // Use directory path here | 77 | + | fsys := os.DirFS(configDir) |
| 78 | + | BuildVersion = config.General.BuildVersion // Use directory path here | |||
| 78 | err := parser.OpenYamlFile(fsys, configFile, &config) // Assuming it doesn't need the last argument | 79 | err := parser.OpenYamlFile(fsys, configFile, &config) // Assuming it doesn't need the last argument | ||
| 79 | return config, err | 80 | return config, err | ||
| 80 | } | 81 | } | ||
M internal/gotemplate/models/html/admin/manager.gohtml +23 -34
| @@ -1,42 +1,31 @@ | |||
| 1 | 1 | <!-- manager.gohtml --> | |
| 2 | 2 | <!DOCTYPE html> | |
| 3 | 3 | <html lang="en"> | |
| 4 | + | {{template "header.gohtml" .}} | |
| 4 | 5 | ||
| 5 | - | {{template "header.gohtml" .}} | |
| 6 | - | ||
| 7 | - | <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-8 p-0"> | |
| 8 | - | {{template "leftmenu.gohtml" .}} | |
| 9 | - | <!-- Main content here --> | |
| 10 | - | <!-- This div will contain the error message when something goes wrong --> | |
| 11 | - | <div id="container-images-error" style="display:none;"> | |
| 12 | - | Something went wrong. </div> | |
| 13 | - | <div class="flex-1 flex flex-col space-y-4 p-4 md:ml-4"> | |
| 14 | - | <div class="flex flex-col md:flex-row w-full flex-grow md:mt-24"> | |
| 15 | - | <div class="flex-grow bg-white p-4 rounded-md shadow mb-4"> | |
| 16 | - | <h1>Container Manager</h1> | |
| 17 | - | <!-- List all the images on load with HTMX --> | |
| 18 | - | <div id="container-images" hx-get="/image-manager" hx-trigger="load" hx-swap="innerHTML" hx-swap-oob="true" | |
| 19 | - | hx-target-error="container-images-error"> | |
| 20 | - | {{range .Images}} | |
| 21 | - | <div class="image-info"> | |
| 22 | - | <p>ID: {{.ID}}</p> | |
| 23 | - | <p>Repository: {{.Repository}}</p> | |
| 24 | - | <p>Tag: {{.Tag}}</p> | |
| 25 | - | <p>Created: {{.Created}}</p> | |
| 26 | - | <p>Size: {{.Size}}</p> | |
| 27 | - | </div> | |
| 28 | - | {{else}} | |
| 29 | - | <p>No images found.</p> | |
| 30 | - | {{end}} | |
| 6 | + | <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-8 p-0"> | |
| 7 | + | {{template "leftmenu.gohtml" .}} | |
| 8 | + | <!-- Main content here --> | |
| 9 | + | <!-- This div will contain the error message when something goes wrong --> | |
| 10 | + | <div class="flex-1 flex flex-col space-y-4 p-4 md:ml-4"> | |
| 11 | + | <div class="flex flex-col md:flex-row w-full flex-grow md:mt-24"> | |
| 12 | + | <div class="flex-grow bg-white p-4 rounded-md shadow mb-4"> | |
| 13 | + | <h1 class="text-lg md:text-2xl font-bold">Container Manager</h1> | |
| 14 | + | <!-- List all the images on load with HTMX --> | |
| 15 | + | <div | |
| 16 | + | id="container-images" | |
| 17 | + | hx-get="/htmx/image-manager" | |
| 18 | + | hx-trigger="load" | |
| 19 | + | hx-swap="innerHTML" | |
| 20 | + | hx-swap-oob="true" | |
| 21 | + | hx-target-error="container-list-images-error" | |
| 22 | + | ></div> | |
| 23 | + | <div id="container-list-images-error" style="display: none"></div> | |
| 31 | 24 | </div> | |
| 32 | - | ||
| 33 | - | ||
| 34 | 25 | </div> | |
| 35 | 26 | </div> | |
| 36 | - | </div> | |
| 37 | - | ||
| 38 | - | <!-- footer --> | |
| 39 | - | {{template "footer.gohtml" .}} | |
| 40 | - | </body> | |
| 41 | 27 | ||
| 42 | - | </html>⏎̸ | |
| 28 | + | <!-- footer --> | |
| 29 | + | {{template "footer.gohtml" .}} | |
| 30 | + | </body> | |
| 31 | + | </html> | |
| @@ -1,42 +1,31 @@ | |||||
| 1 | <!-- manager.gohtml --> | 1 | <!-- manager.gohtml --> | ||
| 2 | <!DOCTYPE html> | 2 | <!DOCTYPE html> | ||
| 3 | <html lang="en"> | 3 | <html lang="en"> | ||
| 4 | + | {{template "header.gohtml" .}} | |||
| 4 | 5 | ||||
| 5 | - | {{template "header.gohtml" .}} | 6 | + | <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-8 p-0"> |
| 6 | - | 7 | + | {{template "leftmenu.gohtml" .}} | |
| 7 | - | <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-8 p-0"> | 8 | + | <!-- Main content here --> |
| 8 | - | {{template "leftmenu.gohtml" .}} | 9 | + | <!-- This div will contain the error message when something goes wrong --> |
| 9 | - | <!-- Main content here --> | 10 | + | <div class="flex-1 flex flex-col space-y-4 p-4 md:ml-4"> |
| 10 | - | <!-- This div will contain the error message when something goes wrong --> | 11 | + | <div class="flex flex-col md:flex-row w-full flex-grow md:mt-24"> |
| 11 | - | <div id="container-images-error" style="display:none;"> | 12 | + | <div class="flex-grow bg-white p-4 rounded-md shadow mb-4"> |
| 12 | - | Something went wrong. </div> | 13 | + | <h1 class="text-lg md:text-2xl font-bold">Container Manager</h1> |
| 13 | - | <div class="flex-1 flex flex-col space-y-4 p-4 md:ml-4"> | 14 | + | <!-- List all the images on load with HTMX --> |
| 14 | - | <div class="flex flex-col md:flex-row w-full flex-grow md:mt-24"> | 15 | + | <div |
| 15 | - | <div class="flex-grow bg-white p-4 rounded-md shadow mb-4"> | 16 | + | id="container-images" |
| 16 | - | <h1>Container Manager</h1> | 17 | + | hx-get="/htmx/image-manager" |
| 17 | - | <!-- List all the images on load with HTMX --> | 18 | + | hx-trigger="load" |
| 18 | - | <div id="container-images" hx-get="/image-manager" hx-trigger="load" hx-swap="innerHTML" hx-swap-oob="true" | 19 | + | hx-swap="innerHTML" |
| 19 | - | hx-target-error="container-images-error"> | 20 | + | hx-swap-oob="true" |
| 20 | - | {{range .Images}} | 21 | + | hx-target-error="container-list-images-error" |
| 21 | - | <div class="image-info"> | 22 | + | ></div> |
| 22 | - | <p>ID: {{.ID}}</p> | 23 | + | <div id="container-list-images-error" style="display: none"></div> |
| 23 | - | <p>Repository: {{.Repository}}</p> | |||
| 24 | - | <p>Tag: {{.Tag}}</p> | |||
| 25 | - | <p>Created: {{.Created}}</p> | |||
| 26 | - | <p>Size: {{.Size}}</p> | |||
| 27 | - | </div> | |||
| 28 | - | {{else}} | |||
| 29 | - | <p>No images found.</p> | |||
| 30 | - | {{end}} | |||
| 31 | </div> | 24 | </div> | ||
| 32 | - | ||||
| 33 | - | ||||
| 34 | </div> | 25 | </div> | ||
| 35 | </div> | 26 | </div> | ||
| 36 | - | </div> | |||
| 37 | - | ||||
| 38 | - | <!-- footer --> | |||
| 39 | - | {{template "footer.gohtml" .}} | |||
| 40 | - | </body> | |||
| 41 | 27 | ||||
| 42 | - | </html>⏎̸ | 28 | + | <!-- footer --> |
| 29 | + | {{template "footer.gohtml" .}} | |||
| 30 | + | </body> | |||
| 31 | + | </html> | |||
R internal/gotemplate/models/html/components.gohtml → internal/gotemplate/models/html/fragments/components.gohtml +0 -0 renamed, 100% similar
renamed, 100% similar
A internal/gotemplate/models/html/fragments/image_list.gohtml +34 -0
| @@ -0,0 +1,34 @@ | |||
| 1 | + | <div class="flex flex-col p-4"> | |
| 2 | + | <h1 class="text-lg md:text-xl font-bold">Images</h1> | |
| 3 | + | <div class="flex font-bold border-t-2 align-baseline py-2"> | |
| 4 | + | <div class="w-2/4">Name</div> | |
| 5 | + | <div class="w-2/4">Created</div> | |
| 6 | + | <div class="w-1/6">Size</div> | |
| 7 | + | <div class="w-1/4">Action</div> | |
| 8 | + | </div> | |
| 9 | + | {{ range.Images }} | |
| 10 | + | <div id="{{.ShortID}}" class="flex py-2 align-baseline justify-center"> | |
| 11 | + | <div class="w-2/4">{{ range.RepoTags }}{{.}}<br />{{ end }}</div> | |
| 12 | + | <div class="w-2/4">{{.CreatedStr}}</div> | |
| 13 | + | <div class="w-1/6">{{.SizeStr}}</div> | |
| 14 | + | <div class="w-1/4"> | |
| 15 | + | <button | |
| 16 | + | hx-trigger="click" | |
| 17 | + | hx-delete="/htmx/image-manager/delete/{{.ShortID}}" | |
| 18 | + | hx-swap="innerHTML" | |
| 19 | + | hx-target-error="container-delete-images-error" | |
| 20 | + | class="bg-red-500 text-white px-2 py-1 rounded" | |
| 21 | + | > | |
| 22 | + | Remove | |
| 23 | + | </button> | |
| 24 | + | </div> | |
| 25 | + | </div> | |
| 26 | + | {{ else }} | |
| 27 | + | <div class="mt-2">No images found.</div> | |
| 28 | + | {{ end }} | |
| 29 | + | <div | |
| 30 | + | id="container-delete-images-error" | |
| 31 | + | style="display: none" | |
| 32 | + | class="mt-2 text-red-800 text-lg" | |
| 33 | + | ></div> | |
| 34 | + | </div> | |
| @@ -0,0 +1,34 @@ | |||||
| 1 | + | <div class="flex flex-col p-4"> | |||
| 2 | + | <h1 class="text-lg md:text-xl font-bold">Images</h1> | |||
| 3 | + | <div class="flex font-bold border-t-2 align-baseline py-2"> | |||
| 4 | + | <div class="w-2/4">Name</div> | |||
| 5 | + | <div class="w-2/4">Created</div> | |||
| 6 | + | <div class="w-1/6">Size</div> | |||
| 7 | + | <div class="w-1/4">Action</div> | |||
| 8 | + | </div> | |||
| 9 | + | {{ range.Images }} | |||
| 10 | + | <div id="{{.ShortID}}" class="flex py-2 align-baseline justify-center"> | |||
| 11 | + | <div class="w-2/4">{{ range.RepoTags }}{{.}}<br />{{ end }}</div> | |||
| 12 | + | <div class="w-2/4">{{.CreatedStr}}</div> | |||
| 13 | + | <div class="w-1/6">{{.SizeStr}}</div> | |||
| 14 | + | <div class="w-1/4"> | |||
| 15 | + | <button | |||
| 16 | + | hx-trigger="click" | |||
| 17 | + | hx-delete="/htmx/image-manager/delete/{{.ShortID}}" | |||
| 18 | + | hx-swap="innerHTML" | |||
| 19 | + | hx-target-error="container-delete-images-error" | |||
| 20 | + | class="bg-red-500 text-white px-2 py-1 rounded" | |||
| 21 | + | > | |||
| 22 | + | Remove | |||
| 23 | + | </button> | |||
| 24 | + | </div> | |||
| 25 | + | </div> | |||
| 26 | + | {{ else }} | |||
| 27 | + | <div class="mt-2">No images found.</div> | |||
| 28 | + | {{ end }} | |||
| 29 | + | <div | |||
| 30 | + | id="container-delete-images-error" | |||
| 31 | + | style="display: none" | |||
| 32 | + | class="mt-2 text-red-800 text-lg" | |||
| 33 | + | ></div> | |||
| 34 | + | </div> | |||
M internal/gotemplate/models/html/fragments/leftmenu.gohtml +1 -2
| @@ -9,8 +9,7 @@ | |||
| 9 | 9 | </div> | |
| 10 | 10 | <div class="w-full"> | |
| 11 | 11 | <ul | |
| 12 | - | class="flex flex-wrap md:flex-col md:space-y-6 md:py-8 font-semibold text-gray-100 text-lg md:text-2xl md:pl-4 min-w-fit" | |
| 13 | - | hx-boost="true"> | |
| 12 | + | class="flex flex-wrap md:flex-col md:space-y-6 md:py-8 font-semibold text-gray-100 text-lg md:text-2xl md:pl-4 min-w-fit"> | |
| 14 | 13 | <li class="flex items-center w-1/2 md:w-full"> | |
| 15 | 14 | <i class="iconoir-dashboard-speed mr-2 md:text-3xl"></i> | |
| 16 | 15 | <a href="/admin">Dashboard</a> | |
| @@ -9,8 +9,7 @@ | |||||
| 9 | </div> | 9 | </div> | ||
| 10 | <div class="w-full"> | 10 | <div class="w-full"> | ||
| 11 | <ul | 11 | <ul | ||
| 12 | - | class="flex flex-wrap md:flex-col md:space-y-6 md:py-8 font-semibold text-gray-100 text-lg md:text-2xl md:pl-4 min-w-fit" | 12 | + | class="flex flex-wrap md:flex-col md:space-y-6 md:py-8 font-semibold text-gray-100 text-lg md:text-2xl md:pl-4 min-w-fit"> |
| 13 | - | hx-boost="true"> | |||
| 14 | <li class="flex items-center w-1/2 md:w-full"> | 13 | <li class="flex items-center w-1/2 md:w-full"> | ||
| 15 | <i class="iconoir-dashboard-speed mr-2 md:text-3xl"></i> | 14 | <i class="iconoir-dashboard-speed mr-2 md:text-3xl"></i> | ||
| 16 | <a href="/admin">Dashboard</a> | 15 | <a href="/admin">Dashboard</a> | ||
D internal/gotemplate/models/html/fragments/manager.gohtml +0 -0
no textual change.
M internal/httpserve/handler/adminroutes.go +14 -0
| @@ -8,6 +8,8 @@ import ( | |||
| 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" | |
| 11 | 13 | "github.com/labstack/echo/v4" | |
| 12 | 14 | ) | |
| 13 | 15 | ||
| @@ -60,9 +62,21 @@ func AdminManagerRoute(c echo.Context, a *app.App) error { | |||
| 60 | 62 | return err | |
| 61 | 63 | } | |
| 62 | 64 | ||
| 65 | + | images, err := docker.ListContainerImages() | |
| 66 | + | if err != nil { | |
| 67 | + | rawErrHTML := `<div id="container-images">Error: ` + err.Error() + `</div>` | |
| 68 | + | sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML) | |
| 69 | + | if err != nil { | |
| 70 | + | // Handle the sanitization error | |
| 71 | + | return c.String(http.StatusInternalServerError, "An error occurred during sanitization") | |
| 72 | + | } | |
| 73 | + | return c.HTML(http.StatusInternalServerError, sanitizedHTML) | |
| 74 | + | } | |
| 75 | + | ||
| 63 | 76 | // Create a data map to pass to the renderer | |
| 64 | 77 | data := map[string]interface{}{ | |
| 65 | 78 | "CurrentLang": yamlData.CurrentLang, | |
| 79 | + | "Images": images, | |
| 66 | 80 | } | |
| 67 | 81 | ||
| 68 | 82 | renderedHTML, err := rendererData.Render(data, a) | |
| @@ -8,6 +8,8 @@ import ( | |||||
| 8 | "github.com/bnema/gordon/internal/gotemplate/render" | 8 | "github.com/bnema/gordon/internal/gotemplate/render" | ||
| 9 | "github.com/bnema/gordon/internal/httpserve/middleware" | 9 | "github.com/bnema/gordon/internal/httpserve/middleware" | ||
| 10 | "github.com/bnema/gordon/internal/webui" | 10 | "github.com/bnema/gordon/internal/webui" | ||
| 11 | + | "github.com/bnema/gordon/pkg/utils/docker" | |||
| 12 | + | "github.com/bnema/gordon/pkg/utils/sanitize" | |||
| 11 | "github.com/labstack/echo/v4" | 13 | "github.com/labstack/echo/v4" | ||
| 12 | ) | 14 | ) | ||
| 13 | 15 | ||||
| @@ -60,9 +62,21 @@ func AdminManagerRoute(c echo.Context, a *app.App) error { | |||||
| 60 | return err | 62 | return err | ||
| 61 | } | 63 | } | ||
| 62 | 64 | ||||
| 65 | + | images, err := docker.ListContainerImages() | |||
| 66 | + | if err != nil { | |||
| 67 | + | rawErrHTML := `<div id="container-images">Error: ` + err.Error() + `</div>` | |||
| 68 | + | sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML) | |||
| 69 | + | if err != nil { | |||
| 70 | + | // Handle the sanitization error | |||
| 71 | + | return c.String(http.StatusInternalServerError, "An error occurred during sanitization") | |||
| 72 | + | } | |||
| 73 | + | return c.HTML(http.StatusInternalServerError, sanitizedHTML) | |||
| 74 | + | } | |||
| 75 | + | ||||
| 63 | // Create a data map to pass to the renderer | 76 | // Create a data map to pass to the renderer | ||
| 64 | data := map[string]interface{}{ | 77 | data := map[string]interface{}{ | ||
| 65 | "CurrentLang": yamlData.CurrentLang, | 78 | "CurrentLang": yamlData.CurrentLang, | ||
| 79 | + | "Images": images, | |||
| 66 | } | 80 | } | ||
| 67 | 81 | ||||
| 68 | renderedHTML, err := rendererData.Render(data, a) | 82 | renderedHTML, err := rendererData.Render(data, a) | ||
M internal/httpserve/handler/docker.go +68 -8
| @@ -1,31 +1,91 @@ | |||
| 1 | 1 | package handler | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "fmt" | |
| 4 | 5 | "net/http" | |
| 6 | + | "time" | |
| 5 | 7 | ||
| 8 | + | "github.com/bnema/gordon/internal/app" | |
| 9 | + | "github.com/bnema/gordon/internal/gotemplate/render" | |
| 6 | 10 | "github.com/bnema/gordon/pkg/utils/docker" | |
| 11 | + | "github.com/bnema/gordon/pkg/utils/humanize" | |
| 7 | 12 | "github.com/bnema/gordon/pkg/utils/sanitize" | |
| 8 | 13 | "github.com/labstack/echo/v4" | |
| 9 | 14 | ) | |
| 10 | 15 | ||
| 11 | - | func ImageManagerHandler(c echo.Context) error { | |
| 12 | - | // Retrieve the list of container images. | |
| 16 | + | var idMap = make(map[string]string) | |
| 17 | + | ||
| 18 | + | type HumanReadableContainerImage struct { | |
| 19 | + | docker.ContainerImage | |
| 20 | + | ShortID string | |
| 21 | + | CreatedStr string | |
| 22 | + | SizeStr string | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // ImageManagerHandler handles the /image-manager route | |
| 26 | + | func ImageManagerHandler(c echo.Context, a *app.App) error { | |
| 13 | 27 | images, err := docker.ListContainerImages() | |
| 14 | 28 | if err != nil { | |
| 15 | - | rawErrHTML := `<div id="container-images">Error: ` + err.Error() + `</div>` | |
| 29 | + | rawErrHTML := `<div>Error: ` + err.Error() + `</div>` | |
| 16 | 30 | sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML) | |
| 17 | 31 | if err != nil { | |
| 18 | - | // Handle the sanitization error | |
| 19 | 32 | return c.String(http.StatusInternalServerError, "An error occurred during sanitization") | |
| 20 | 33 | } | |
| 21 | 34 | return c.HTML(http.StatusInternalServerError, sanitizedHTML) | |
| 22 | 35 | } | |
| 23 | 36 | ||
| 24 | - | // Create a map to hold the data you want to pass to the template. | |
| 37 | + | var humanReadableImages []HumanReadableContainerImage | |
| 38 | + | ||
| 39 | + | for _, image := range images { | |
| 40 | + | ShortID := image.ID[7:19] | |
| 41 | + | idMap[ShortID] = image.ID | |
| 42 | + | createdTime := time.Unix(image.Created, 0) | |
| 43 | + | createdStr := humanize.TimeAgo(createdTime) | |
| 44 | + | sizeStr := humanize.BytesToReadableSize(image.Size) | |
| 45 | + | ||
| 46 | + | humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{ | |
| 47 | + | ShortID: ShortID, | |
| 48 | + | ContainerImage: image, | |
| 49 | + | CreatedStr: createdStr, | |
| 50 | + | SizeStr: sizeStr, | |
| 51 | + | }) | |
| 52 | + | } | |
| 25 | 53 | data := map[string]interface{}{ | |
| 26 | - | "Images": images, | |
| 54 | + | "Images": humanReadableImages, | |
| 27 | 55 | } | |
| 28 | 56 | ||
| 29 | - | // Render the template with the data. | |
| 30 | - | return c.Render(http.StatusOK, "manager", data) | |
| 57 | + | rendererData, err := render.GetHTMLRenderer("html/fragments", "image_list.gohtml", a.TemplateFS, a) | |
| 58 | + | if err != nil { | |
| 59 | + | return err | |
| 60 | + | } | |
| 61 | + | renderedHTML, err := rendererData.Render(data, a) | |
| 62 | + | if err != nil { | |
| 63 | + | return err | |
| 64 | + | } | |
| 65 | + | return c.HTML(200, renderedHTML) | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | // ImageManagerDeleteHandler handles the /image-manager/delete route | |
| 69 | + | func ImageManagerDeleteHandler(c echo.Context, a *app.App) error { | |
| 70 | + | // Get the ShortID from the URL | |
| 71 | + | ShortID := c.Param("ShortID") | |
| 72 | + | // Check if the shortID exists in the idMap | |
| 73 | + | imageID, exists := idMap[ShortID] | |
| 74 | + | if !exists { | |
| 75 | + | return c.String(http.StatusBadRequest, "Invalid shortID") | |
| 76 | + | } | |
| 77 | + | fmt.Println("ShortID:", ShortID, "ImageID:", imageID) | |
| 78 | + | ||
| 79 | + | err := docker.DeleteContainerImage(imageID) | |
| 80 | + | if err != nil { | |
| 81 | + | rawErrHTML := `<div>Error: ` + err.Error() + `</div>` | |
| 82 | + | sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML) | |
| 83 | + | if err != nil { | |
| 84 | + | // Handle the sanitization error | |
| 85 | + | return c.String(http.StatusInternalServerError, "An error occurred during sanitization") | |
| 86 | + | } | |
| 87 | + | return c.HTML(http.StatusInternalServerError, sanitizedHTML) | |
| 88 | + | } | |
| 89 | + | // Since it is HTMX we return a html div with the message "Removed" | |
| 90 | + | return c.HTML(http.StatusOK, `<div>Removed</div>`) | |
| 31 | 91 | } | |
| @@ -1,31 +1,91 @@ | |||||
| 1 | package handler | 1 | package handler | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "fmt" | |||
| 4 | "net/http" | 5 | "net/http" | ||
| 6 | + | "time" | |||
| 5 | 7 | ||||
| 8 | + | "github.com/bnema/gordon/internal/app" | |||
| 9 | + | "github.com/bnema/gordon/internal/gotemplate/render" | |||
| 6 | "github.com/bnema/gordon/pkg/utils/docker" | 10 | "github.com/bnema/gordon/pkg/utils/docker" | ||
| 11 | + | "github.com/bnema/gordon/pkg/utils/humanize" | |||
| 7 | "github.com/bnema/gordon/pkg/utils/sanitize" | 12 | "github.com/bnema/gordon/pkg/utils/sanitize" | ||
| 8 | "github.com/labstack/echo/v4" | 13 | "github.com/labstack/echo/v4" | ||
| 9 | ) | 14 | ) | ||
| 10 | 15 | ||||
| 11 | - | func ImageManagerHandler(c echo.Context) error { | 16 | + | var idMap = make(map[string]string) |
| 12 | - | // Retrieve the list of container images. | 17 | + | |
| 18 | + | type HumanReadableContainerImage struct { | |||
| 19 | + | docker.ContainerImage | |||
| 20 | + | ShortID string | |||
| 21 | + | CreatedStr string | |||
| 22 | + | SizeStr string | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // ImageManagerHandler handles the /image-manager route | |||
| 26 | + | func ImageManagerHandler(c echo.Context, a *app.App) error { | |||
| 13 | images, err := docker.ListContainerImages() | 27 | images, err := docker.ListContainerImages() | ||
| 14 | if err != nil { | 28 | if err != nil { | ||
| 15 | - | rawErrHTML := `<div id="container-images">Error: ` + err.Error() + `</div>` | 29 | + | rawErrHTML := `<div>Error: ` + err.Error() + `</div>` |
| 16 | sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML) | 30 | sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML) | ||
| 17 | if err != nil { | 31 | if err != nil { | ||
| 18 | - | // Handle the sanitization error | |||
| 19 | return c.String(http.StatusInternalServerError, "An error occurred during sanitization") | 32 | return c.String(http.StatusInternalServerError, "An error occurred during sanitization") | ||
| 20 | } | 33 | } | ||
| 21 | return c.HTML(http.StatusInternalServerError, sanitizedHTML) | 34 | return c.HTML(http.StatusInternalServerError, sanitizedHTML) | ||
| 22 | } | 35 | } | ||
| 23 | 36 | ||||
| 24 | - | // Create a map to hold the data you want to pass to the template. | 37 | + | var humanReadableImages []HumanReadableContainerImage |
| 38 | + | ||||
| 39 | + | for _, image := range images { | |||
| 40 | + | ShortID := image.ID[7:19] | |||
| 41 | + | idMap[ShortID] = image.ID | |||
| 42 | + | createdTime := time.Unix(image.Created, 0) | |||
| 43 | + | createdStr := humanize.TimeAgo(createdTime) | |||
| 44 | + | sizeStr := humanize.BytesToReadableSize(image.Size) | |||
| 45 | + | ||||
| 46 | + | humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{ | |||
| 47 | + | ShortID: ShortID, | |||
| 48 | + | ContainerImage: image, | |||
| 49 | + | CreatedStr: createdStr, | |||
| 50 | + | SizeStr: sizeStr, | |||
| 51 | + | }) | |||
| 52 | + | } | |||
| 25 | data := map[string]interface{}{ | 53 | data := map[string]interface{}{ | ||
| 26 | - | "Images": images, | 54 | + | "Images": humanReadableImages, |
| 27 | } | 55 | } | ||
| 28 | 56 | ||||
| 29 | - | // Render the template with the data. | 57 | + | rendererData, err := render.GetHTMLRenderer("html/fragments", "image_list.gohtml", a.TemplateFS, a) |
| 30 | - | return c.Render(http.StatusOK, "manager", data) | 58 | + | if err != nil { |
| 59 | + | return err | |||
| 60 | + | } | |||
| 61 | + | renderedHTML, err := rendererData.Render(data, a) | |||
| 62 | + | if err != nil { | |||
| 63 | + | return err | |||
| 64 | + | } | |||
| 65 | + | return c.HTML(200, renderedHTML) | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | // ImageManagerDeleteHandler handles the /image-manager/delete route | |||
| 69 | + | func ImageManagerDeleteHandler(c echo.Context, a *app.App) error { | |||
| 70 | + | // Get the ShortID from the URL | |||
| 71 | + | ShortID := c.Param("ShortID") | |||
| 72 | + | // Check if the shortID exists in the idMap | |||
| 73 | + | imageID, exists := idMap[ShortID] | |||
| 74 | + | if !exists { | |||
| 75 | + | return c.String(http.StatusBadRequest, "Invalid shortID") | |||
| 76 | + | } | |||
| 77 | + | fmt.Println("ShortID:", ShortID, "ImageID:", imageID) | |||
| 78 | + | ||||
| 79 | + | err := docker.DeleteContainerImage(imageID) | |||
| 80 | + | if err != nil { | |||
| 81 | + | rawErrHTML := `<div>Error: ` + err.Error() + `</div>` | |||
| 82 | + | sanitizedHTML, err := sanitize.SanitizeHTML(rawErrHTML) | |||
| 83 | + | if err != nil { | |||
| 84 | + | // Handle the sanitization error | |||
| 85 | + | return c.String(http.StatusInternalServerError, "An error occurred during sanitization") | |||
| 86 | + | } | |||
| 87 | + | return c.HTML(http.StatusInternalServerError, sanitizedHTML) | |||
| 88 | + | } | |||
| 89 | + | // Since it is HTMX we return a html div with the message "Removed" | |||
| 90 | + | return c.HTML(http.StatusOK, `<div>Removed</div>`) | |||
| 31 | } | 91 | } | ||
M internal/httpserve/router.go +10 -1
| @@ -26,7 +26,7 @@ func RegisterRoutes(e *echo.Echo, a *app.App) *echo.Echo { | |||
| 26 | 26 | bindAdminRoute(e, a, AdminPath) | |
| 27 | 27 | bindStaticRoute(e, a, "/*") | |
| 28 | 28 | bindLoginRoute(e, a, AdminPath) | |
| 29 | - | e.GET("/image-manager", handler.ImageManagerHandler) | |
| 29 | + | bindHTMXEndpoints(e, a) | |
| 30 | 30 | // Protect the root path with a 403 | |
| 31 | 31 | e.GET("/", func(c echo.Context) error { | |
| 32 | 32 | return c.String(403, "403 Forbidden") | |
| @@ -65,3 +65,12 @@ func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) { | |||
| 65 | 65 | return handler.Logout(c, a) | |
| 66 | 66 | }) | |
| 67 | 67 | } | |
| 68 | + | ||
| 69 | + | func bindHTMXEndpoints(e *echo.Echo, a *app.App) { | |
| 70 | + | e.GET("/htmx/image-manager", func(c echo.Context) error { | |
| 71 | + | return handler.ImageManagerHandler(c, a) | |
| 72 | + | }) | |
| 73 | + | e.DELETE("/htmx/image-manager/delete/:ShortID", func(c echo.Context) error { | |
| 74 | + | return handler.ImageManagerDeleteHandler(c, a) | |
| 75 | + | }) | |
| 76 | + | } | |
| @@ -26,7 +26,7 @@ func RegisterRoutes(e *echo.Echo, a *app.App) *echo.Echo { | |||||
| 26 | bindAdminRoute(e, a, AdminPath) | 26 | bindAdminRoute(e, a, AdminPath) | ||
| 27 | bindStaticRoute(e, a, "/*") | 27 | bindStaticRoute(e, a, "/*") | ||
| 28 | bindLoginRoute(e, a, AdminPath) | 28 | bindLoginRoute(e, a, AdminPath) | ||
| 29 | - | e.GET("/image-manager", handler.ImageManagerHandler) | 29 | + | bindHTMXEndpoints(e, a) |
| 30 | // Protect the root path with a 403 | 30 | // Protect the root path with a 403 | ||
| 31 | e.GET("/", func(c echo.Context) error { | 31 | e.GET("/", func(c echo.Context) error { | ||
| 32 | return c.String(403, "403 Forbidden") | 32 | return c.String(403, "403 Forbidden") | ||
| @@ -65,3 +65,12 @@ func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) { | |||||
| 65 | return handler.Logout(c, a) | 65 | return handler.Logout(c, a) | ||
| 66 | }) | 66 | }) | ||
| 67 | } | 67 | } | ||
| 68 | + | ||||
| 69 | + | func bindHTMXEndpoints(e *echo.Echo, a *app.App) { | |||
| 70 | + | e.GET("/htmx/image-manager", func(c echo.Context) error { | |||
| 71 | + | return handler.ImageManagerHandler(c, a) | |||
| 72 | + | }) | |||
| 73 | + | e.DELETE("/htmx/image-manager/delete/:ShortID", func(c echo.Context) error { | |||
| 74 | + | return handler.ImageManagerDeleteHandler(c, a) | |||
| 75 | + | }) | |||
| 76 | + | } | |||
M internal/webui/public/assets/css/tailwind.css +55 -14
| @@ -558,8 +558,8 @@ video { | |||
| 558 | 558 | margin-right: 0.5rem; | |
| 559 | 559 | } | |
| 560 | 560 | ||
| 561 | - | .mt-20 { | |
| 562 | - | margin-top: 5rem; | |
| 561 | + | .mt-2 { | |
| 562 | + | margin-top: 0.5rem; | |
| 563 | 563 | } | |
| 564 | 564 | ||
| 565 | 565 | .mt-4 { | |
| @@ -602,10 +602,22 @@ video { | |||
| 602 | 602 | width: 50%; | |
| 603 | 603 | } | |
| 604 | 604 | ||
| 605 | + | .w-1\/4 { | |
| 606 | + | width: 25%; | |
| 607 | + | } | |
| 608 | + | ||
| 609 | + | .w-1\/6 { | |
| 610 | + | width: 16.666667%; | |
| 611 | + | } | |
| 612 | + | ||
| 605 | 613 | .w-2\/3 { | |
| 606 | 614 | width: 66.666667%; | |
| 607 | 615 | } | |
| 608 | 616 | ||
| 617 | + | .w-2\/4 { | |
| 618 | + | width: 50%; | |
| 619 | + | } | |
| 620 | + | ||
| 609 | 621 | .w-72 { | |
| 610 | 622 | width: 18rem; | |
| 611 | 623 | } | |
| @@ -627,10 +639,6 @@ video { | |||
| 627 | 639 | flex-grow: 1; | |
| 628 | 640 | } | |
| 629 | 641 | ||
| 630 | - | .transform { | |
| 631 | - | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); | |
| 632 | - | } | |
| 633 | - | ||
| 634 | 642 | .flex-row { | |
| 635 | 643 | flex-direction: row; | |
| 636 | 644 | } | |
| @@ -675,6 +683,10 @@ video { | |||
| 675 | 683 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); | |
| 676 | 684 | } | |
| 677 | 685 | ||
| 686 | + | .rounded { | |
| 687 | + | border-radius: 0.25rem; | |
| 688 | + | } | |
| 689 | + | ||
| 678 | 690 | .rounded-lg { | |
| 679 | 691 | border-radius: 0.5rem; | |
| 680 | 692 | } | |
| @@ -691,6 +703,15 @@ video { | |||
| 691 | 703 | border-width: 1px; | |
| 692 | 704 | } | |
| 693 | 705 | ||
| 706 | + | .border-y-2 { | |
| 707 | + | border-top-width: 2px; | |
| 708 | + | border-bottom-width: 2px; | |
| 709 | + | } | |
| 710 | + | ||
| 711 | + | .border-t-2 { | |
| 712 | + | border-top-width: 2px; | |
| 713 | + | } | |
| 714 | + | ||
| 694 | 715 | .border-gray-300 { | |
| 695 | 716 | --tw-border-opacity: 1; | |
| 696 | 717 | border-color: rgb(209 213 219 / var(--tw-border-opacity)); | |
| @@ -711,6 +732,11 @@ video { | |||
| 711 | 732 | background-color: rgb(209 213 219 / var(--tw-bg-opacity)); | |
| 712 | 733 | } | |
| 713 | 734 | ||
| 735 | + | .bg-red-500 { | |
| 736 | + | --tw-bg-opacity: 1; | |
| 737 | + | background-color: rgb(239 68 68 / var(--tw-bg-opacity)); | |
| 738 | + | } | |
| 739 | + | ||
| 714 | 740 | .bg-transparent { | |
| 715 | 741 | background-color: transparent; | |
| 716 | 742 | } | |
| @@ -747,11 +773,20 @@ video { | |||
| 747 | 773 | padding-right: 1rem; | |
| 748 | 774 | } | |
| 749 | 775 | ||
| 776 | + | .py-1 { | |
| 777 | + | padding-top: 0.25rem; | |
| 778 | + | padding-bottom: 0.25rem; | |
| 779 | + | } | |
| 780 | + | ||
| 750 | 781 | .py-2 { | |
| 751 | 782 | padding-top: 0.5rem; | |
| 752 | 783 | padding-bottom: 0.5rem; | |
| 753 | 784 | } | |
| 754 | 785 | ||
| 786 | + | .align-baseline { | |
| 787 | + | vertical-align: baseline; | |
| 788 | + | } | |
| 789 | + | ||
| 755 | 790 | .text-2xl { | |
| 756 | 791 | font-size: 1.5rem; | |
| 757 | 792 | line-height: 2rem; | |
| @@ -777,6 +812,10 @@ video { | |||
| 777 | 812 | line-height: 1.25rem; | |
| 778 | 813 | } | |
| 779 | 814 | ||
| 815 | + | .font-bold { | |
| 816 | + | font-weight: 700; | |
| 817 | + | } | |
| 818 | + | ||
| 780 | 819 | .font-semibold { | |
| 781 | 820 | font-weight: 600; | |
| 782 | 821 | } | |
| @@ -805,6 +844,11 @@ video { | |||
| 805 | 844 | color: rgb(31 41 55 / var(--tw-text-opacity)); | |
| 806 | 845 | } | |
| 807 | 846 | ||
| 847 | + | .text-red-800 { | |
| 848 | + | --tw-text-opacity: 1; | |
| 849 | + | color: rgb(153 27 27 / var(--tw-text-opacity)); | |
| 850 | + | } | |
| 851 | + | ||
| 808 | 852 | .text-slate-100 { | |
| 809 | 853 | --tw-text-opacity: 1; | |
| 810 | 854 | color: rgb(241 245 249 / var(--tw-text-opacity)); | |
| @@ -826,14 +870,6 @@ video { | |||
| 826 | 870 | outline-offset: 2px; | |
| 827 | 871 | } | |
| 828 | 872 | ||
| 829 | - | .transition { | |
| 830 | - | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; | |
| 831 | - | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; | |
| 832 | - | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; | |
| 833 | - | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | |
| 834 | - | transition-duration: 150ms; | |
| 835 | - | } | |
| 836 | - | ||
| 837 | 873 | /* Import IBM Plex Fonts */ | |
| 838 | 874 | ||
| 839 | 875 | @font-face { | |
| @@ -1045,6 +1081,11 @@ video { | |||
| 1045 | 1081 | font-size: 1.875rem; | |
| 1046 | 1082 | line-height: 2.25rem; | |
| 1047 | 1083 | } | |
| 1084 | + | ||
| 1085 | + | .md\:text-xl { | |
| 1086 | + | font-size: 1.25rem; | |
| 1087 | + | line-height: 1.75rem; | |
| 1088 | + | } | |
| 1048 | 1089 | } | |
| 1049 | 1090 | ||
| 1050 | 1091 | @media (min-width: 1024px) { | |
| @@ -558,8 +558,8 @@ video { | |||||
| 558 | margin-right: 0.5rem; | 558 | margin-right: 0.5rem; | ||
| 559 | } | 559 | } | ||
| 560 | 560 | ||||
| 561 | - | .mt-20 { | 561 | + | .mt-2 { |
| 562 | - | margin-top: 5rem; | 562 | + | margin-top: 0.5rem; |
| 563 | } | 563 | } | ||
| 564 | 564 | ||||
| 565 | .mt-4 { | 565 | .mt-4 { | ||
| @@ -602,10 +602,22 @@ video { | |||||
| 602 | width: 50%; | 602 | width: 50%; | ||
| 603 | } | 603 | } | ||
| 604 | 604 | ||||
| 605 | + | .w-1\/4 { | |||
| 606 | + | width: 25%; | |||
| 607 | + | } | |||
| 608 | + | ||||
| 609 | + | .w-1\/6 { | |||
| 610 | + | width: 16.666667%; | |||
| 611 | + | } | |||
| 612 | + | ||||
| 605 | .w-2\/3 { | 613 | .w-2\/3 { | ||
| 606 | width: 66.666667%; | 614 | width: 66.666667%; | ||
| 607 | } | 615 | } | ||
| 608 | 616 | ||||
| 617 | + | .w-2\/4 { | |||
| 618 | + | width: 50%; | |||
| 619 | + | } | |||
| 620 | + | ||||
| 609 | .w-72 { | 621 | .w-72 { | ||
| 610 | width: 18rem; | 622 | width: 18rem; | ||
| 611 | } | 623 | } | ||
| @@ -627,10 +639,6 @@ video { | |||||
| 627 | flex-grow: 1; | 639 | flex-grow: 1; | ||
| 628 | } | 640 | } | ||
| 629 | 641 | ||||
| 630 | - | .transform { | |||
| 631 | - | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); | |||
| 632 | - | } | |||
| 633 | - | ||||
| 634 | .flex-row { | 642 | .flex-row { | ||
| 635 | flex-direction: row; | 643 | flex-direction: row; | ||
| 636 | } | 644 | } | ||
| @@ -675,6 +683,10 @@ video { | |||||
| 675 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); | 683 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); | ||
| 676 | } | 684 | } | ||
| 677 | 685 | ||||
| 686 | + | .rounded { | |||
| 687 | + | border-radius: 0.25rem; | |||
| 688 | + | } | |||
| 689 | + | ||||
| 678 | .rounded-lg { | 690 | .rounded-lg { | ||
| 679 | border-radius: 0.5rem; | 691 | border-radius: 0.5rem; | ||
| 680 | } | 692 | } | ||
| @@ -691,6 +703,15 @@ video { | |||||
| 691 | border-width: 1px; | 703 | border-width: 1px; | ||
| 692 | } | 704 | } | ||
| 693 | 705 | ||||
| 706 | + | .border-y-2 { | |||
| 707 | + | border-top-width: 2px; | |||
| 708 | + | border-bottom-width: 2px; | |||
| 709 | + | } | |||
| 710 | + | ||||
| 711 | + | .border-t-2 { | |||
| 712 | + | border-top-width: 2px; | |||
| 713 | + | } | |||
| 714 | + | ||||
| 694 | .border-gray-300 { | 715 | .border-gray-300 { | ||
| 695 | --tw-border-opacity: 1; | 716 | --tw-border-opacity: 1; | ||
| 696 | border-color: rgb(209 213 219 / var(--tw-border-opacity)); | 717 | border-color: rgb(209 213 219 / var(--tw-border-opacity)); | ||
| @@ -711,6 +732,11 @@ video { | |||||
| 711 | background-color: rgb(209 213 219 / var(--tw-bg-opacity)); | 732 | background-color: rgb(209 213 219 / var(--tw-bg-opacity)); | ||
| 712 | } | 733 | } | ||
| 713 | 734 | ||||
| 735 | + | .bg-red-500 { | |||
| 736 | + | --tw-bg-opacity: 1; | |||
| 737 | + | background-color: rgb(239 68 68 / var(--tw-bg-opacity)); | |||
| 738 | + | } | |||
| 739 | + | ||||
| 714 | .bg-transparent { | 740 | .bg-transparent { | ||
| 715 | background-color: transparent; | 741 | background-color: transparent; | ||
| 716 | } | 742 | } | ||
| @@ -747,11 +773,20 @@ video { | |||||
| 747 | padding-right: 1rem; | 773 | padding-right: 1rem; | ||
| 748 | } | 774 | } | ||
| 749 | 775 | ||||
| 776 | + | .py-1 { | |||
| 777 | + | padding-top: 0.25rem; | |||
| 778 | + | padding-bottom: 0.25rem; | |||
| 779 | + | } | |||
| 780 | + | ||||
| 750 | .py-2 { | 781 | .py-2 { | ||
| 751 | padding-top: 0.5rem; | 782 | padding-top: 0.5rem; | ||
| 752 | padding-bottom: 0.5rem; | 783 | padding-bottom: 0.5rem; | ||
| 753 | } | 784 | } | ||
| 754 | 785 | ||||
| 786 | + | .align-baseline { | |||
| 787 | + | vertical-align: baseline; | |||
| 788 | + | } | |||
| 789 | + | ||||
| 755 | .text-2xl { | 790 | .text-2xl { | ||
| 756 | font-size: 1.5rem; | 791 | font-size: 1.5rem; | ||
| 757 | line-height: 2rem; | 792 | line-height: 2rem; | ||
| @@ -777,6 +812,10 @@ video { | |||||
| 777 | line-height: 1.25rem; | 812 | line-height: 1.25rem; | ||
| 778 | } | 813 | } | ||
| 779 | 814 | ||||
| 815 | + | .font-bold { | |||
| 816 | + | font-weight: 700; | |||
| 817 | + | } | |||
| 818 | + | ||||
| 780 | .font-semibold { | 819 | .font-semibold { | ||
| 781 | font-weight: 600; | 820 | font-weight: 600; | ||
| 782 | } | 821 | } | ||
| @@ -805,6 +844,11 @@ video { | |||||
| 805 | color: rgb(31 41 55 / var(--tw-text-opacity)); | 844 | color: rgb(31 41 55 / var(--tw-text-opacity)); | ||
| 806 | } | 845 | } | ||
| 807 | 846 | ||||
| 847 | + | .text-red-800 { | |||
| 848 | + | --tw-text-opacity: 1; | |||
| 849 | + | color: rgb(153 27 27 / var(--tw-text-opacity)); | |||
| 850 | + | } | |||
| 851 | + | ||||
| 808 | .text-slate-100 { | 852 | .text-slate-100 { | ||
| 809 | --tw-text-opacity: 1; | 853 | --tw-text-opacity: 1; | ||
| 810 | color: rgb(241 245 249 / var(--tw-text-opacity)); | 854 | color: rgb(241 245 249 / var(--tw-text-opacity)); | ||
| @@ -826,14 +870,6 @@ video { | |||||
| 826 | outline-offset: 2px; | 870 | outline-offset: 2px; | ||
| 827 | } | 871 | } | ||
| 828 | 872 | ||||
| 829 | - | .transition { | |||
| 830 | - | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; | |||
| 831 | - | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; | |||
| 832 | - | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; | |||
| 833 | - | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | |||
| 834 | - | transition-duration: 150ms; | |||
| 835 | - | } | |||
| 836 | - | ||||
| 837 | /* Import IBM Plex Fonts */ | 873 | /* Import IBM Plex Fonts */ | ||
| 838 | 874 | ||||
| 839 | @font-face { | 875 | @font-face { | ||
| @@ -1045,6 +1081,11 @@ video { | |||||
| 1045 | font-size: 1.875rem; | 1081 | font-size: 1.875rem; | ||
| 1046 | line-height: 2.25rem; | 1082 | line-height: 2.25rem; | ||
| 1047 | } | 1083 | } | ||
| 1084 | + | ||||
| 1085 | + | .md\:text-xl { | |||
| 1086 | + | font-size: 1.25rem; | |||
| 1087 | + | line-height: 1.75rem; | |||
| 1088 | + | } | |||
| 1048 | } | 1089 | } | ||
| 1049 | 1090 | ||||
| 1050 | @media (min-width: 1024px) { | 1091 | @media (min-width: 1024px) { | ||
M pkg/utils/docker/initclient.go +52 -42
| @@ -3,21 +3,21 @@ package docker | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | - | "net" | |
| 7 | - | "net/http" | |
| 8 | - | "net/url" | |
| 9 | - | "time" | |
| 10 | 6 | ||
| 11 | 7 | "github.com/docker/docker/api/types" | |
| 12 | 8 | "github.com/docker/docker/client" | |
| 13 | 9 | ) | |
| 14 | 10 | ||
| 15 | 11 | type ContainerImage struct { | |
| 16 | - | ID string | |
| 17 | - | Repository string | |
| 18 | - | Tag string | |
| 19 | - | Created time.Time | |
| 20 | - | Size int64 | |
| 12 | + | ID string `json:"Id"` | |
| 13 | + | ParentID string `json:",omitempty"` | |
| 14 | + | RepoTags []string `json:",omitempty"` | |
| 15 | + | RepoDigests []string `json:",omitempty"` | |
| 16 | + | Created int64 | |
| 17 | + | Size int64 | |
| 18 | + | SharedSize int64 | |
| 19 | + | Labels map[string]string `json:",omitempty"` | |
| 20 | + | Containers int64 | |
| 21 | 21 | } | |
| 22 | 22 | ||
| 23 | 23 | type ContainerEngineClient interface { | |
| @@ -36,26 +36,19 @@ var dockerCli *client.Client | |||
| 36 | 36 | type DockerClient struct{} | |
| 37 | 37 | ||
| 38 | 38 | func (d *DockerClient) InitializeClient(config *Config) error { | |
| 39 | - | // Create a custom HTTP client that uses the Docker socket | |
| 40 | - | httpClient := &http.Client{ | |
| 41 | - | Transport: &http.Transport{ | |
| 42 | - | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { | |
| 43 | - | return net.Dial("unix", config.Sock) | |
| 44 | - | }, | |
| 45 | - | }, | |
| 39 | + | // Validate if the Sock field is not empty | |
| 40 | + | if config.Sock == "" { | |
| 41 | + | return fmt.Errorf("Sock field in Config is empty") | |
| 46 | 42 | } | |
| 47 | 43 | ||
| 48 | 44 | // Initialize Docker client | |
| 49 | 45 | cli, err := client.NewClientWithOpts( | |
| 50 | - | client.WithHTTPClient(httpClient), | |
| 51 | 46 | client.WithAPIVersionNegotiation(), | |
| 52 | - | client.WithScheme("unix"), | |
| 53 | - | client.WithHost(url.PathEscape(config.Sock)), | |
| 47 | + | client.WithHost("unix://"+config.Sock), // Prepend "unix://" to the Unix socket path | |
| 54 | 48 | ) | |
| 55 | 49 | if err != nil { | |
| 56 | - | return err | |
| 50 | + | return fmt.Errorf("Error from DockerClient: %s", err) | |
| 57 | 51 | } | |
| 58 | - | ||
| 59 | 52 | dockerCli = cli | |
| 60 | 53 | return nil | |
| 61 | 54 | } | |
| @@ -76,30 +69,47 @@ func ListContainerImages() ([]ContainerImage, error) { | |||
| 76 | 69 | var containerImages []ContainerImage | |
| 77 | 70 | for _, image := range images { | |
| 78 | 71 | containerImages = append(containerImages, ContainerImage{ | |
| 79 | - | ID: image.ID, | |
| 80 | - | // Repository and Tag information may require additional processing | |
| 81 | - | Created: time.Unix(image.Created, 0), | |
| 82 | - | Size: image.Size, | |
| 72 | + | ID: image.ID, | |
| 73 | + | ParentID: image.ParentID, | |
| 74 | + | RepoTags: image.RepoTags, | |
| 75 | + | RepoDigests: image.RepoDigests, | |
| 76 | + | Created: image.Created, | |
| 77 | + | Size: image.Size, | |
| 78 | + | SharedSize: image.SharedSize, | |
| 79 | + | Labels: image.Labels, | |
| 80 | + | Containers: image.Containers, | |
| 83 | 81 | }) | |
| 84 | 82 | } | |
| 85 | 83 | ||
| 86 | 84 | return containerImages, nil | |
| 87 | 85 | } | |
| 88 | 86 | ||
| 89 | - | // // PodmanClient implements the ContainerEngineClient interface for Podman | |
| 90 | - | // type PodmanClient struct{} | |
| 91 | - | ||
| 92 | - | // func (p *PodmanClient) InitializeClient(config *Config) error { | |
| 93 | - | // // Initialize Podman client here | |
| 94 | - | // return nil // Return an error if something goes wrong | |
| 95 | - | // } | |
| 96 | - | // func InitializeEngineClient(config *Config) { | |
| 97 | - | // if config.PodmanEnable { | |
| 98 | - | // engineClient = &PodmanClient{} | |
| 99 | - | // } else { | |
| 100 | - | // engineClient = &DockerClient{} | |
| 101 | - | // } | |
| 102 | - | // if err := engineClient.InitializeClient(config); err != nil { | |
| 103 | - | // log.Fatalf("Error initializing container engine client: %v", err) | |
| 104 | - | // } | |
| 105 | - | // } | |
| 87 | + | func ListRunningContainers() ([]types.Container, error) { | |
| 88 | + | // Check if the Docker client has been initialized | |
| 89 | + | if dockerCli == nil { | |
| 90 | + | return nil, fmt.Errorf("Docker client has not been initialized") | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | // List containers using the Docker client | |
| 94 | + | containers, err := dockerCli.ContainerList(context.Background(), types.ContainerListOptions{}) | |
| 95 | + | if err != nil { | |
| 96 | + | return nil, err | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | return containers, nil | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func DeleteContainerImage(imageID string) error { | |
| 103 | + | // Check if the Docker client has been initialized | |
| 104 | + | if dockerCli == nil { | |
| 105 | + | return fmt.Errorf("Docker client has not been initialized") | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | // Delete the image using the Docker client | |
| 109 | + | _, err := dockerCli.ImageRemove(context.Background(), imageID, types.ImageRemoveOptions{}) | |
| 110 | + | if err != nil { | |
| 111 | + | return err | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | return nil | |
| 115 | + | } | |
| @@ -3,21 +3,21 @@ package docker | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | - | "net" | |||
| 7 | - | "net/http" | |||
| 8 | - | "net/url" | |||
| 9 | - | "time" | |||
| 10 | 6 | ||||
| 11 | "github.com/docker/docker/api/types" | 7 | "github.com/docker/docker/api/types" | ||
| 12 | "github.com/docker/docker/client" | 8 | "github.com/docker/docker/client" | ||
| 13 | ) | 9 | ) | ||
| 14 | 10 | ||||
| 15 | type ContainerImage struct { | 11 | type ContainerImage struct { | ||
| 16 | - | ID string | 12 | + | ID string `json:"Id"` |
| 17 | - | Repository string | 13 | + | ParentID string `json:",omitempty"` |
| 18 | - | Tag string | 14 | + | RepoTags []string `json:",omitempty"` |
| 19 | - | Created time.Time | 15 | + | RepoDigests []string `json:",omitempty"` |
| 20 | - | Size int64 | 16 | + | Created int64 |
| 17 | + | Size int64 | |||
| 18 | + | SharedSize int64 | |||
| 19 | + | Labels map[string]string `json:",omitempty"` | |||
| 20 | + | Containers int64 | |||
| 21 | } | 21 | } | ||
| 22 | 22 | ||||
| 23 | type ContainerEngineClient interface { | 23 | type ContainerEngineClient interface { | ||
| @@ -36,26 +36,19 @@ var dockerCli *client.Client | |||||
| 36 | type DockerClient struct{} | 36 | type DockerClient struct{} | ||
| 37 | 37 | ||||
| 38 | func (d *DockerClient) InitializeClient(config *Config) error { | 38 | func (d *DockerClient) InitializeClient(config *Config) error { | ||
| 39 | - | // Create a custom HTTP client that uses the Docker socket | 39 | + | // Validate if the Sock field is not empty |
| 40 | - | httpClient := &http.Client{ | 40 | + | if config.Sock == "" { |
| 41 | - | Transport: &http.Transport{ | 41 | + | return fmt.Errorf("Sock field in Config is empty") |
| 42 | - | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { | |||
| 43 | - | return net.Dial("unix", config.Sock) | |||
| 44 | - | }, | |||
| 45 | - | }, | |||
| 46 | } | 42 | } | ||
| 47 | 43 | ||||
| 48 | // Initialize Docker client | 44 | // Initialize Docker client | ||
| 49 | cli, err := client.NewClientWithOpts( | 45 | cli, err := client.NewClientWithOpts( | ||
| 50 | - | client.WithHTTPClient(httpClient), | |||
| 51 | client.WithAPIVersionNegotiation(), | 46 | client.WithAPIVersionNegotiation(), | ||
| 52 | - | client.WithScheme("unix"), | 47 | + | client.WithHost("unix://"+config.Sock), // Prepend "unix://" to the Unix socket path |
| 53 | - | client.WithHost(url.PathEscape(config.Sock)), | |||
| 54 | ) | 48 | ) | ||
| 55 | if err != nil { | 49 | if err != nil { | ||
| 56 | - | return err | 50 | + | return fmt.Errorf("Error from DockerClient: %s", err) |
| 57 | } | 51 | } | ||
| 58 | - | ||||
| 59 | dockerCli = cli | 52 | dockerCli = cli | ||
| 60 | return nil | 53 | return nil | ||
| 61 | } | 54 | } | ||
| @@ -76,30 +69,47 @@ func ListContainerImages() ([]ContainerImage, error) { | |||||
| 76 | var containerImages []ContainerImage | 69 | var containerImages []ContainerImage | ||
| 77 | for _, image := range images { | 70 | for _, image := range images { | ||
| 78 | containerImages = append(containerImages, ContainerImage{ | 71 | containerImages = append(containerImages, ContainerImage{ | ||
| 79 | - | ID: image.ID, | 72 | + | ID: image.ID, |
| 80 | - | // Repository and Tag information may require additional processing | 73 | + | ParentID: image.ParentID, |
| 81 | - | Created: time.Unix(image.Created, 0), | 74 | + | RepoTags: image.RepoTags, |
| 82 | - | Size: image.Size, | 75 | + | RepoDigests: image.RepoDigests, |
| 76 | + | Created: image.Created, | |||
| 77 | + | Size: image.Size, | |||
| 78 | + | SharedSize: image.SharedSize, | |||
| 79 | + | Labels: image.Labels, | |||
| 80 | + | Containers: image.Containers, | |||
| 83 | }) | 81 | }) | ||
| 84 | } | 82 | } | ||
| 85 | 83 | ||||
| 86 | return containerImages, nil | 84 | return containerImages, nil | ||
| 87 | } | 85 | } | ||
| 88 | 86 | ||||
| 89 | - | // // PodmanClient implements the ContainerEngineClient interface for Podman | 87 | + | func ListRunningContainers() ([]types.Container, error) { |
| 90 | - | // type PodmanClient struct{} | 88 | + | // Check if the Docker client has been initialized |
| 91 | - | 89 | + | if dockerCli == nil { | |
| 92 | - | // func (p *PodmanClient) InitializeClient(config *Config) error { | 90 | + | return nil, fmt.Errorf("Docker client has not been initialized") |
| 93 | - | // // Initialize Podman client here | 91 | + | } |
| 94 | - | // return nil // Return an error if something goes wrong | 92 | + | |
| 95 | - | // } | 93 | + | // List containers using the Docker client |
| 96 | - | // func InitializeEngineClient(config *Config) { | 94 | + | containers, err := dockerCli.ContainerList(context.Background(), types.ContainerListOptions{}) |
| 97 | - | // if config.PodmanEnable { | 95 | + | if err != nil { |
| 98 | - | // engineClient = &PodmanClient{} | 96 | + | return nil, err |
| 99 | - | // } else { | 97 | + | } |
| 100 | - | // engineClient = &DockerClient{} | 98 | + | |
| 101 | - | // } | 99 | + | return containers, nil |
| 102 | - | // if err := engineClient.InitializeClient(config); err != nil { | 100 | + | } |
| 103 | - | // log.Fatalf("Error initializing container engine client: %v", err) | 101 | + | |
| 104 | - | // } | 102 | + | func DeleteContainerImage(imageID string) error { |
| 105 | - | // } | 103 | + | // Check if the Docker client has been initialized |
| 104 | + | if dockerCli == nil { | |||
| 105 | + | return fmt.Errorf("Docker client has not been initialized") | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | // Delete the image using the Docker client | |||
| 109 | + | _, err := dockerCli.ImageRemove(context.Background(), imageID, types.ImageRemoveOptions{}) | |||
| 110 | + | if err != nil { | |||
| 111 | + | return err | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | return nil | |||
| 115 | + | } | |||
M pkg/utils/htmx/htmlfragments.go +1 -1
| @@ -12,7 +12,7 @@ import ( | |||
| 12 | 12 | // GetHTMLFragmentByID returns the HTML fragment with the specified id | |
| 13 | 13 | func GetHTMLFragmentByID(id string, data interface{}, a *app.App) (string, error) { | |
| 14 | 14 | // 1. Render the template | |
| 15 | - | renderer, err := render.GetHTMLRenderer("components.gohtml", a.TemplateFS, a) | |
| 15 | + | renderer, err := render.GetHTMLRenderer("html/fragments", "components.gohtml", a.TemplateFS, a) | |
| 16 | 16 | if err != nil { | |
| 17 | 17 | return "", fmt.Errorf("failed to get renderer: %w", err) | |
| 18 | 18 | } | |
| @@ -12,7 +12,7 @@ import ( | |||||
| 12 | // GetHTMLFragmentByID returns the HTML fragment with the specified id | 12 | // GetHTMLFragmentByID returns the HTML fragment with the specified id | ||
| 13 | func GetHTMLFragmentByID(id string, data interface{}, a *app.App) (string, error) { | 13 | func GetHTMLFragmentByID(id string, data interface{}, a *app.App) (string, error) { | ||
| 14 | // 1. Render the template | 14 | // 1. Render the template | ||
| 15 | - | renderer, err := render.GetHTMLRenderer("components.gohtml", a.TemplateFS, a) | 15 | + | renderer, err := render.GetHTMLRenderer("html/fragments", "components.gohtml", a.TemplateFS, a) |
| 16 | if err != nil { | 16 | if err != nil { | ||
| 17 | return "", fmt.Errorf("failed to get renderer: %w", err) | 17 | return "", fmt.Errorf("failed to get renderer: %w", err) | ||
| 18 | } | 18 | } | ||
A pkg/utils/humanize/humanize.go +48 -0
| @@ -0,0 +1,48 @@ | |||
| 1 | + | package humanize | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "time" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | func BytesToReadableSize(size int64) string { | |
| 9 | + | var unit string | |
| 10 | + | var converted float64 | |
| 11 | + | ||
| 12 | + | if size >= 1<<30 { | |
| 13 | + | unit = "GB" | |
| 14 | + | converted = float64(size) / (1 << 30) | |
| 15 | + | } else if size >= 1<<20 { | |
| 16 | + | unit = "MB" | |
| 17 | + | converted = float64(size) / (1 << 20) | |
| 18 | + | } else if size >= 1<<10 { | |
| 19 | + | unit = "KB" | |
| 20 | + | converted = float64(size) / (1 << 10) | |
| 21 | + | } else { | |
| 22 | + | unit = "B" | |
| 23 | + | converted = float64(size) | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | return fmt.Sprintf("%.2f%s", converted, unit) | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | func TimeAgo(t time.Time) string { | |
| 30 | + | var duration time.Duration | |
| 31 | + | now := time.Now() | |
| 32 | + | if t.After(now) { | |
| 33 | + | duration = t.Sub(now) | |
| 34 | + | } else { | |
| 35 | + | duration = now.Sub(t) | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | switch { | |
| 39 | + | case duration < time.Minute: | |
| 40 | + | return fmt.Sprintf("%d seconds ago", int(duration.Seconds())) | |
| 41 | + | case duration < time.Hour: | |
| 42 | + | return fmt.Sprintf("%d minutes ago", int(duration.Minutes())) | |
| 43 | + | case duration < time.Hour*24: | |
| 44 | + | return fmt.Sprintf("%d hours ago", int(duration.Hours())) | |
| 45 | + | default: | |
| 46 | + | return fmt.Sprintf("%d days ago", int(duration.Hours()/24)) | |
| 47 | + | } | |
| 48 | + | } | |
| @@ -0,0 +1,48 @@ | |||||
| 1 | + | package humanize | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "time" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | func BytesToReadableSize(size int64) string { | |||
| 9 | + | var unit string | |||
| 10 | + | var converted float64 | |||
| 11 | + | ||||
| 12 | + | if size >= 1<<30 { | |||
| 13 | + | unit = "GB" | |||
| 14 | + | converted = float64(size) / (1 << 30) | |||
| 15 | + | } else if size >= 1<<20 { | |||
| 16 | + | unit = "MB" | |||
| 17 | + | converted = float64(size) / (1 << 20) | |||
| 18 | + | } else if size >= 1<<10 { | |||
| 19 | + | unit = "KB" | |||
| 20 | + | converted = float64(size) / (1 << 10) | |||
| 21 | + | } else { | |||
| 22 | + | unit = "B" | |||
| 23 | + | converted = float64(size) | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | return fmt.Sprintf("%.2f%s", converted, unit) | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | func TimeAgo(t time.Time) string { | |||
| 30 | + | var duration time.Duration | |||
| 31 | + | now := time.Now() | |||
| 32 | + | if t.After(now) { | |||
| 33 | + | duration = t.Sub(now) | |||
| 34 | + | } else { | |||
| 35 | + | duration = now.Sub(t) | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | switch { | |||
| 39 | + | case duration < time.Minute: | |||
| 40 | + | return fmt.Sprintf("%d seconds ago", int(duration.Seconds())) | |||
| 41 | + | case duration < time.Hour: | |||
| 42 | + | return fmt.Sprintf("%d minutes ago", int(duration.Minutes())) | |||
| 43 | + | case duration < time.Hour*24: | |||
| 44 | + | return fmt.Sprintf("%d hours ago", int(duration.Hours())) | |||
| 45 | + | default: | |||
| 46 | + | return fmt.Sprintf("%d days ago", int(duration.Hours()/24)) | |||
| 47 | + | } | |||
| 48 | + | } | |||