images.go
| 1 | package views |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | |
| 6 | "hearthforge/internal/config" |
| 7 | "hearthforge/internal/db" |
| 8 | "hearthforge/internal/util" |
| 9 | |
| 10 | g "maragu.dev/gomponents" |
| 11 | . "maragu.dev/gomponents/html" |
| 12 | ) |
| 13 | |
| 14 | // ImageList renders the Images tab: one block per image path with its tags. |
| 15 | // host is what a client puts in front of the image name. |
| 16 | func ImageList(cfg *config.Config, user *db.SessionUser, repo *db.Repo, |
| 17 | tags []db.ImageTag, sizes map[string]int64, |
| 18 | ) g.Node { |
| 19 | host := cfg.PublicHost |
| 20 | isAdmin := user != nil && user.IsAdmin |
| 21 | // Group tags by image path, keeping the sorted order from the query. |
| 22 | var order []string |
| 23 | byImage := map[string][]db.ImageTag{} |
| 24 | for _, t := range tags { |
| 25 | if _, seen := byImage[t.Image]; !seen { |
| 26 | order = append(order, t.Image) |
| 27 | } |
| 28 | byImage[t.Image] = append(byImage[t.Image], t) |
| 29 | } |
| 30 | // Clients lowercase image names, so the tab shows the name they use. |
| 31 | lowerRepo := strings.ToLower(repo.Name) |
| 32 | fullName := func(image string) string { return strings.TrimSuffix(lowerRepo+"/"+image, "/") } |
| 33 | |
| 34 | return Layout(Page{Title: "Images — " + repo.Name, User: user, Cfg: cfg}, |
| 35 | Div(Class("container"), |
| 36 | RepoHeader(repo), |
| 37 | RepoNav(cfg, repo, "images", user), |
| 38 | Div(Class("list-header"), |
| 39 | H2(Class("list-heading"), g.Text("Images")), |
| 40 | g.If(isAdmin && len(order) > 0, Details(Class("confirm-details"), |
| 41 | Summary(Class("btn btn-sm btn-danger"), g.Text("Delete all")), |
| 42 | Div(Class("confirm-popup"), |
| 43 | g.Text("Delete every image and tag of this repository?"), |
| 44 | Form(Method("POST"), Action("/"+repo.Name+"/images/delete-all"), Class("inline-form"), |
| 45 | Button(Type("submit"), Class("btn btn-sm btn-danger"), g.Text("Yes, delete all")), |
| 46 | ), |
| 47 | ), |
| 48 | )), |
| 49 | ), |
| 50 | g.If(len(order) == 0, Div(Class("empty-state"), |
| 51 | P(g.Text("No images yet.")), |
| 52 | g.If(isAdmin, P(Class("text-muted"), |
| 53 | g.Text("Push one with "), |
| 54 | Code(g.Text("docker push "+host+"/"+lowerRepo+":TAG")), |
| 55 | g.Text("."))), |
| 56 | )), |
| 57 | g.If(len(order) > 0, Ul(Class("issue-list"), |
| 58 | g.Map(order, func(image string) g.Node { |
| 59 | return Li(Class("issue-item"), |
| 60 | Div(Class("release-item-header"), |
| 61 | Div(Class("release-item-main"), |
| 62 | Code(Class("release-item-title"), g.Text(host+"/"+fullName(image))), |
| 63 | Div(Class("release-item-meta"), |
| 64 | Span(g.Text(countLabel(len(byImage[image]), "tag", "tags"))), |
| 65 | Span(g.Text(util.FormatBytes(sizes[image]))), |
| 66 | ), |
| 67 | ), |
| 68 | g.If(isAdmin, Details(Class("confirm-details"), |
| 69 | Summary(Class("btn btn-sm btn-danger"), g.Text("Delete image")), |
| 70 | Div(Class("confirm-popup"), |
| 71 | g.Text("Delete every tag and layer of "+fullName(image)+"?"), |
| 72 | Form(Method("POST"), Action("/"+repo.Name+"/images/delete"), Class("inline-form"), |
| 73 | Input(Type("hidden"), Name("image"), Value(image)), |
| 74 | Button(Type("submit"), Class("btn btn-sm btn-danger"), g.Text("Yes, delete")), |
| 75 | ), |
| 76 | ), |
| 77 | )), |
| 78 | ), |
| 79 | Ul(Class("image-tags"), |
| 80 | g.Map(byImage[image], func(t db.ImageTag) g.Node { |
| 81 | return Li(Class("image-tag"), |
| 82 | Span(Class("badge"), g.Text(t.Tag)), |
| 83 | // Digests passed the registry's regex, so 19 chars is "sha256:" plus 12 hex. |
| 84 | Code(Class("image-digest"), Title(t.Digest), g.Text(t.Digest[:19])), |
| 85 | g.El("time", Class("text-muted"), DateTime(t.UpdatedAt), g.Text(util.FormatDate(t.UpdatedAt))), |
| 86 | g.If(isAdmin, Form(Method("POST"), Action("/"+repo.Name+"/images/delete"), |
| 87 | Class("inline-form image-tag-delete"), |
| 88 | Input(Type("hidden"), Name("image"), Value(image)), |
| 89 | Input(Type("hidden"), Name("tag"), Value(t.Tag)), |
| 90 | Button(Type("submit"), Class("btn btn-xs"), g.Text("Delete tag")), |
| 91 | )), |
| 92 | ) |
| 93 | }), |
| 94 | ), |
| 95 | ) |
| 96 | }), |
| 97 | )), |
| 98 | ), |
| 99 | ) |
| 100 | } |
| 101 |