go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/internal/ui/common.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 "context" 19 "embed" 20 "errors" 21 "fmt" 22 "html/template" 23 "io/fs" 24 "net/http" 25 "time" 26 27 "github.com/dustin/go-humanize" 28 29 "go.chromium.org/luci/auth/identity" 30 "go.chromium.org/luci/common/clock" 31 cfgcommonpb "go.chromium.org/luci/common/proto/config" 32 "go.chromium.org/luci/server" 33 "go.chromium.org/luci/server/auth" 34 "go.chromium.org/luci/server/auth/xsrf" 35 "go.chromium.org/luci/server/router" 36 "go.chromium.org/luci/server/templates" 37 bootstrap "go.chromium.org/luci/web/third_party/bootstrap/v5" 38 39 "go.chromium.org/luci/config_service/internal/importer" 40 configpb "go.chromium.org/luci/config_service/proto" 41 ) 42 43 //go:embed pages includes 44 var templateFS embed.FS 45 46 //go:embed static 47 var staticFS embed.FS 48 49 var pageStartTimeContextKey = "start time for the frontend page" 50 var configsServerContextKey = "configsServer for the front page" 51 52 // InstallHandlers adds HTTP handlers that render HTML pages. 53 func InstallHandlers(srv *server.Server, configsSrv configpb.ConfigsServer, importer importer.Importer) { 54 m := router.NewMiddlewareChain( 55 func(c *router.Context, next router.Handler) { 56 reqCtx := c.Request.Context() 57 reqCtx = context.WithValue(reqCtx, &pageStartTimeContextKey, clock.Now(reqCtx)) 58 reqCtx = context.WithValue(reqCtx, &configsServerContextKey, configsSrv) 59 c.Request = c.Request.WithContext(reqCtx) 60 next(c) 61 }, 62 templates.WithTemplates(prepareTemplates(&srv.Options)), 63 auth.Authenticate(srv.CookieAuth), 64 ) 65 switch sFS, err := fs.Sub(staticFS, "static"); { 66 case err != nil: 67 panic(fmt.Errorf("failed to return a subFS for static directory: %w", err)) 68 default: 69 // staticFS has "static" directory at top level. We need to make the child 70 // directories inside the "static" directory top level directory. 71 srv.Routes.Static("/static", nil, http.FS(sFS)) 72 } 73 srv.Routes.Static("/third_party/bootstrap", nil, http.FS(bootstrap.FS)) 74 75 srv.Routes.GET("/", m, renderErr(indexPage)) 76 srv.Routes.GET("/config_set/*ConfigSet", m, renderErr(configSetPage)) 77 srv.Routes.POST("/internal/frontend/reimport/*ConfigSet", m, importer.Reimport) 78 } 79 80 // prepareTemplates configures templates.Bundle used by all UI handlers. 81 // 82 // In particular it includes a set of default arguments passed to all templates. 83 func prepareTemplates(opts *server.Options) *templates.Bundle { 84 return &templates.Bundle{ 85 Loader: templates.FileSystemLoader(templateFS), 86 DebugMode: func(context.Context) bool { return !opts.Prod }, 87 DefaultTemplate: "base", 88 FuncMap: template.FuncMap{ 89 "RelTime": func(ts, now time.Time) string { 90 return humanize.RelTime(ts, now, "ago", "from now") 91 }, 92 "HumanizeBytes": func(size int64) string { 93 return humanize.Bytes(uint64(size)) 94 }, 95 "AttemptStatus": attemptStatus, 96 }, 97 DefaultArgs: func(ctx context.Context, e *templates.Extra) (templates.Args, error) { 98 loginURL, err := auth.LoginURL(ctx, e.Request.URL.RequestURI()) 99 if err != nil { 100 return nil, err 101 } 102 logoutURL, err := auth.LogoutURL(ctx, e.Request.URL.RequestURI()) 103 if err != nil { 104 return nil, err 105 } 106 token, err := xsrf.Token(ctx) 107 if err != nil { 108 return nil, err 109 } 110 return templates.Args{ 111 "ImageVersion": opts.ImageVersion(), 112 "IsAnonymous": auth.CurrentIdentity(ctx) == identity.AnonymousIdentity, 113 "User": auth.CurrentUser(ctx), 114 "LoginURL": loginURL, 115 "LogoutURL": logoutURL, 116 "XsrfToken": token, 117 "Now": startTime(ctx), 118 "HandlerDuration": func() time.Duration { 119 return clock.Now(ctx).Sub(startTime(ctx)) 120 }, 121 }, nil 122 }, 123 } 124 } 125 126 func startTime(c context.Context) time.Time { 127 ts, ok := c.Value(&pageStartTimeContextKey).(time.Time) 128 if !ok { 129 panic(errors.New("impossible; pageStartTimeContextKey is not set")) 130 } 131 return ts 132 } 133 134 func configsServer(c context.Context) configpb.ConfigsServer { 135 return c.Value(&configsServerContextKey).(configpb.ConfigsServer) 136 } 137 138 func attemptStatus(attempt *configpb.ConfigSet_Attempt) string { 139 if !attempt.Success { 140 return "failed" 141 } 142 for _, msg := range attempt.GetValidationResult().GetMessages() { 143 if msg.GetSeverity() == cfgcommonpb.ValidationResult_WARNING { 144 return "warning" 145 } 146 } 147 return "success" 148 }