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  }