go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/scheduler/appengine/ui/common.go (about)

     1  // Copyright 2015 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 implements request handlers that serve user facing HTML pages.
    16  package ui
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"html/template"
    22  	"os"
    23  	"strings"
    24  	"time"
    25  
    26  	"go.chromium.org/luci/gae/service/info"
    27  
    28  	"go.chromium.org/luci/appengine/gaeauth/server"
    29  	"go.chromium.org/luci/common/clock"
    30  	"go.chromium.org/luci/server/auth"
    31  	"go.chromium.org/luci/server/auth/xsrf"
    32  	"go.chromium.org/luci/server/router"
    33  	"go.chromium.org/luci/server/templates"
    34  
    35  	"go.chromium.org/luci/scheduler/appengine/catalog"
    36  	"go.chromium.org/luci/scheduler/appengine/engine"
    37  )
    38  
    39  // Config is global configuration of UI handlers.
    40  type Config struct {
    41  	Engine        engine.Engine
    42  	Catalog       catalog.Catalog
    43  	TemplatesPath string // path to templates directory deployed to GAE
    44  }
    45  
    46  // InstallHandlers adds HTTP handlers that render HTML pages.
    47  func InstallHandlers(r *router.Router, base router.MiddlewareChain, cfg Config) {
    48  	tmpl := prepareTemplates(cfg.TemplatesPath)
    49  
    50  	m := base.Extend(func(c *router.Context, next router.Handler) {
    51  		ctx := context.WithValue(c.Request.Context(), configContextKey(0), &cfg)
    52  		c.Request = c.Request.WithContext(context.WithValue(ctx, startTimeContextKey(0), clock.Now(ctx)))
    53  		next(c)
    54  	})
    55  	m = m.Extend(
    56  		templates.WithTemplates(tmpl),
    57  		auth.Authenticate(server.UsersAPIAuthMethod{}),
    58  	)
    59  
    60  	r.GET("/", m, indexPage)
    61  	r.GET("/jobs/:ProjectID", m, projectPage)
    62  	r.GET("/jobs/:ProjectID/:JobName", m, jobPage)
    63  	r.GET("/jobs/:ProjectID/:JobName/:InvID", m, invocationPage)
    64  
    65  	// All POST forms must be protected with XSRF token.
    66  	mxsrf := m.Extend(xsrf.WithTokenCheck)
    67  	r.POST("/actions/triggerJob/:ProjectID/:JobName", mxsrf, triggerJobAction)
    68  	r.POST("/actions/pauseJob/:ProjectID/:JobName", mxsrf, pauseJobAction)
    69  	r.POST("/actions/resumeJob/:ProjectID/:JobName", mxsrf, resumeJobAction)
    70  	r.POST("/actions/abortJob/:ProjectID/:JobName", mxsrf, abortJobAction)
    71  	r.POST("/actions/abortInvocation/:ProjectID/:JobName/:InvID", mxsrf, abortInvocationAction)
    72  }
    73  
    74  type configContextKey int
    75  
    76  // config returns Config passed to InstallHandlers.
    77  func config(c context.Context) *Config {
    78  	cfg, _ := c.Value(configContextKey(0)).(*Config)
    79  	if cfg == nil {
    80  		panic("impossible, configContextKey is not set")
    81  	}
    82  	return cfg
    83  }
    84  
    85  type startTimeContextKey int
    86  
    87  // startTime returns timestamp when we started handling the request.
    88  func startTime(c context.Context) time.Time {
    89  	ts, ok := c.Value(startTimeContextKey(0)).(time.Time)
    90  	if !ok {
    91  		panic("impossible, startTimeContextKey is not set")
    92  	}
    93  	return ts
    94  }
    95  
    96  // prepareTemplates configures templates.Bundle used by all UI handlers.
    97  //
    98  // In particular it includes a set of default arguments passed to all templates.
    99  func prepareTemplates(templatesPath string) *templates.Bundle {
   100  	return &templates.Bundle{
   101  		Loader:          templates.FileSystemLoader(os.DirFS(templatesPath)),
   102  		DebugMode:       info.IsDevAppServer,
   103  		DefaultTemplate: "base",
   104  		FuncMap: template.FuncMap{
   105  			// Count returns sequential integers in range [0, n] (inclusive).
   106  			"Count": func(n int) []int {
   107  				out := make([]int, n+1)
   108  				for i := range out {
   109  					out[i] = i
   110  				}
   111  				return out
   112  			},
   113  			// Pair combines two args into one map with keys "First" and "Second", to
   114  			// pass pairs to templates (that in golang can accept only one argument).
   115  			"Pair": func(a1, a2 any) map[string]any {
   116  				return map[string]any{
   117  					"First":  a1,
   118  					"Second": a2,
   119  				}
   120  			},
   121  			// JobCount returns "<n> job(s)".
   122  			"JobCount": func(jobs sortedJobs) string {
   123  				switch len(jobs) {
   124  				case 0:
   125  					return "NONE"
   126  				case 1:
   127  					return "1 JOB"
   128  				default:
   129  					return fmt.Sprintf("%d JOBS", len(jobs))
   130  				}
   131  			},
   132  		},
   133  		DefaultArgs: func(c context.Context, e *templates.Extra) (templates.Args, error) {
   134  			loginURL, err := auth.LoginURL(c, e.Request.URL.RequestURI())
   135  			if err != nil {
   136  				return nil, err
   137  			}
   138  			logoutURL, err := auth.LogoutURL(c, e.Request.URL.RequestURI())
   139  			if err != nil {
   140  				return nil, err
   141  			}
   142  			token, err := xsrf.Token(c)
   143  			if err != nil {
   144  				return nil, err
   145  			}
   146  			return templates.Args{
   147  				"AppVersion":  strings.Split(info.VersionID(c), ".")[0],
   148  				"IsAnonymous": auth.CurrentIdentity(c) == "anonymous:anonymous",
   149  				"User":        auth.CurrentUser(c),
   150  				"LoginURL":    loginURL,
   151  				"LogoutURL":   logoutURL,
   152  				"XsrfToken":   token,
   153  				"HandlerDuration": func() time.Duration {
   154  					return clock.Now(c).Sub(startTime(c))
   155  				},
   156  			}, nil
   157  		},
   158  	}
   159  }