go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipd/appengine/ui/listing.go (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package ui 16 17 import ( 18 "strings" 19 20 "github.com/dustin/go-humanize" 21 "golang.org/x/sync/errgroup" 22 "google.golang.org/grpc/codes" 23 "google.golang.org/grpc/status" 24 25 "go.chromium.org/luci/common/clock" 26 "go.chromium.org/luci/server/router" 27 "go.chromium.org/luci/server/templates" 28 29 api "go.chromium.org/luci/cipd/api/cipd/v1" 30 "go.chromium.org/luci/cipd/common" 31 ) 32 33 const rootPackage = "" 34 35 func listingPage(c *router.Context, pkg string) error { 36 pkg = strings.Trim(pkg, "/") 37 if pkg != rootPackage { 38 if err := common.ValidatePackageName(pkg); err != nil { 39 return status.Errorf(codes.InvalidArgument, "%s", err) 40 } 41 } 42 43 // Note PublicRepo is essentially CIPD server public API and it checks ACLs 44 // in every call. Thus UI cannot be tricked to reveal anything that is not 45 // already accessible via RPC API. 46 ctx := c.Request.Context() 47 svc := state(ctx).services.PublicRepo 48 49 // Cursor used for paginating instance listing. 50 cursor := c.Request.URL.Query().Get("c") 51 prevPageURL := "" 52 nextPageURL := "" 53 54 eg, gctx := errgroup.WithContext(ctx) 55 56 // Fetch ACLs of to this package to display them. Note this is useful to show 57 // even if there's no such package yet. 58 var meta *prefixMetadataBlock 59 eg.Go(func() error { 60 var err error 61 meta, err = fetchPrefixMetadata(gctx, pkg) 62 return err 63 }) 64 65 // List the children of this prefix. If there are no children, this is a leaf 66 // package and we need fetch siblings instead. Such leaf packages are shown 67 // together with their siblings in the UI in a "highlighted" state. This also 68 // detects non-existing packages or prefixes. 69 var relatives *api.ListPrefixResponse 70 var missing bool 71 eg.Go(func() error { 72 var err error 73 relatives, err = svc.ListPrefix(gctx, &api.ListPrefixRequest{ 74 Prefix: pkg, 75 }) 76 if err != nil || pkg == rootPackage || len(relatives.Packages) != 0 || len(relatives.Prefixes) != 0 { 77 return err 78 } 79 // There are no child packages. Try to find siblings by listing the parent. 80 parent := "" 81 if i := strings.LastIndex(pkg, "/"); i != -1 { 82 parent = pkg[:i] 83 } 84 relatives, err = svc.ListPrefix(gctx, &api.ListPrefixRequest{ 85 Prefix: parent, 86 }) 87 if err != nil { 88 return err 89 } 90 // Check if `pkg` actually exists (as a prefix or package). We should not 91 // show siblings of a missing entity, it looks very confusing. 92 isInList := func(s string, l []string) bool { 93 for _, p := range l { 94 if p == s { 95 return true 96 } 97 } 98 return false 99 } 100 missing = !isInList(pkg, relatives.Packages) && !isInList(pkg, relatives.Prefixes) 101 return nil 102 }) 103 104 // Fetch instance of this package, if it is a package. This will be empty if 105 // it is not a package or it doesn't exist or not visible. Non-existing 106 // entities are already checked by the prefix listing logic above. 107 var instances []*api.Instance 108 if pkg != rootPackage { 109 eg.Go(func() error { 110 resp, err := svc.ListInstances(gctx, &api.ListInstancesRequest{ 111 Package: pkg, 112 PageSize: 12, 113 PageToken: cursor, 114 }) 115 switch status.Code(err) { 116 case codes.OK: // carry on 117 case codes.NotFound, codes.PermissionDenied: 118 return nil 119 default: 120 return err 121 } 122 if resp.NextPageToken != "" { 123 instancesListing.storePrevCursor(gctx, pkg, resp.NextPageToken, cursor) 124 nextPageURL = listingPageURL(pkg, resp.NextPageToken) 125 } 126 if cursor != "" { 127 prevPageURL = listingPageURL(pkg, instancesListing.fetchPrevCursor(gctx, pkg, cursor)) 128 } 129 instances = resp.Instances 130 return nil 131 }) 132 } 133 134 // Fetch refs of this package, if it is a package. 135 var refs []*api.Ref 136 if pkg != rootPackage { 137 eg.Go(func() error { 138 resp, err := svc.ListRefs(gctx, &api.ListRefsRequest{ 139 Package: pkg, 140 }) 141 switch status.Code(err) { 142 case codes.OK: // carry on 143 case codes.NotFound, codes.PermissionDenied: 144 return nil 145 default: 146 return err 147 } 148 refs = resp.Refs 149 return nil 150 }) 151 } 152 153 if err := eg.Wait(); err != nil { 154 return err 155 } 156 157 // Mapping "instance ID" => list of refs pointing to it. 158 refMap := make(map[string][]*api.Ref, len(refs)) 159 for _, ref := range refs { 160 iid := common.ObjectRefToInstanceID(ref.Instance) 161 refMap[iid] = append(refMap[iid], ref) 162 } 163 164 // Build instance listing, annotating instances with refs that point to them. 165 now := clock.Now(ctx).UTC() 166 type instanceItem struct { 167 ID string 168 TruncatedID string 169 Href string 170 Refs []refItem 171 Age string 172 } 173 instListing := make([]instanceItem, len(instances)) 174 for i, inst := range instances { 175 iid := common.ObjectRefToInstanceID(inst.Instance) 176 instListing[i] = instanceItem{ 177 ID: iid, 178 TruncatedID: iid[:30], 179 Href: instancePageURL(pkg, iid), 180 Age: humanize.RelTime(inst.RegisteredTs.AsTime(), now, "", ""), 181 Refs: refsListing(refMap[iid], pkg, now), 182 } 183 } 184 185 templates.MustRender(ctx, c.Writer, "pages/index.html", map[string]any{ 186 "Package": pkg, 187 "Missing": missing, 188 "Breadcrumbs": breadcrumbs(pkg, "", len(instListing) != 0), 189 "Listing": prefixListing(pkg, relatives), 190 "Metadata": meta, 191 "Instances": instListing, 192 "Refs": refsListing(refs, pkg, now), 193 "NextPageURL": nextPageURL, 194 "PrevPageURL": prevPageURL, 195 }) 196 return nil 197 } 198 199 type listingItem struct { 200 Title string 201 Href string 202 Back bool 203 Active bool 204 Prefix bool 205 Package bool 206 } 207 208 func prefixListing(pkg string, relatives *api.ListPrefixResponse) []*listingItem { 209 var listing []*listingItem 210 211 title := func(pfx string) string { 212 if pfx == rootPackage { 213 return "[root]" 214 } 215 return pfx[strings.LastIndex(pfx, "/")+1:] 216 } 217 218 // The "go up" item is always first unless we already at root. 219 if pkg != rootPackage { 220 parent := "" 221 if i := strings.LastIndex(pkg, "/"); i != -1 { 222 parent = pkg[:i] 223 } 224 listing = append(listing, &listingItem{ 225 Back: true, 226 Href: listingPageURL(parent, ""), 227 }) 228 } 229 230 // This will list either children or sibling of the current package (prefixes 231 // and packages), depending on if it is a leaf or not. 232 prefixes := make(map[string]*listingItem, len(relatives.Prefixes)) 233 for _, p := range relatives.Prefixes { 234 item := &listingItem{ 235 Title: title(p), 236 Href: listingPageURL(p, ""), 237 Prefix: true, 238 } 239 listing = append(listing, item) 240 prefixes[p] = item 241 } 242 for _, p := range relatives.Packages { 243 if item := prefixes[p]; item != nil { 244 item.Package = true 245 } else { 246 listing = append(listing, &listingItem{ 247 Title: title(p), 248 Href: listingPageURL(p, ""), 249 Active: p == pkg, // can be true only when listing siblings 250 Package: true, 251 }) 252 } 253 } 254 return listing 255 }