blob: 9836f6aa2be6a1887eadd7ca67c37f6f7c0ed5db [file] [log] [blame]
// Copyright 2023 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ui
import (
"context"
"embed"
"errors"
"fmt"
"html/template"
"io/fs"
"net/http"
"time"
"github.com/dustin/go-humanize"
"go.chromium.org/luci/auth/identity"
"go.chromium.org/luci/common/clock"
cfgcommonpb "go.chromium.org/luci/common/proto/config"
"go.chromium.org/luci/server"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/xsrf"
"go.chromium.org/luci/server/router"
"go.chromium.org/luci/server/templates"
bootstrap "go.chromium.org/luci/web/third_party/bootstrap/v5"
"go.chromium.org/luci/config_service/internal/importer"
configpb "go.chromium.org/luci/config_service/proto"
)
//go:embed pages includes
var templateFS embed.FS
//go:embed static
var staticFS embed.FS
var pageStartTimeContextKey = "start time for the frontend page"
var configsServerContextKey = "configsServer for the front page"
// InstallHandlers adds HTTP handlers that render HTML pages.
func InstallHandlers(srv *server.Server, configsSrv configpb.ConfigsServer, importer importer.Importer) {
m := router.NewMiddlewareChain(
func(c *router.Context, next router.Handler) {
reqCtx := c.Request.Context()
reqCtx = context.WithValue(reqCtx, &pageStartTimeContextKey, clock.Now(reqCtx))
reqCtx = context.WithValue(reqCtx, &configsServerContextKey, configsSrv)
c.Request = c.Request.WithContext(reqCtx)
next(c)
},
templates.WithTemplates(prepareTemplates(&srv.Options)),
auth.Authenticate(srv.CookieAuth),
)
switch sFS, err := fs.Sub(staticFS, "static"); {
case err != nil:
panic(fmt.Errorf("failed to return a subFS for static directory: %w", err))
default:
// staticFS has "static" directory at top level. We need to make the child
// directories inside the "static" directory top level directory.
srv.Routes.Static("/static", nil, http.FS(sFS))
}
srv.Routes.Static("/third_party/bootstrap", nil, http.FS(bootstrap.FS))
srv.Routes.GET("/", m, renderErr(indexPage))
srv.Routes.GET("/config_set/*ConfigSet", m, renderErr(configSetPage))
srv.Routes.POST("/internal/frontend/reimport/*ConfigSet", m, importer.Reimport)
}
// prepareTemplates configures templates.Bundle used by all UI handlers.
//
// In particular it includes a set of default arguments passed to all templates.
func prepareTemplates(opts *server.Options) *templates.Bundle {
return &templates.Bundle{
Loader: templates.FileSystemLoader(templateFS),
DebugMode: func(context.Context) bool { return !opts.Prod },
DefaultTemplate: "base",
FuncMap: template.FuncMap{
"RelTime": func(ts, now time.Time) string {
return humanize.RelTime(ts, now, "ago", "from now")
},
"HumanizeBytes": func(size int64) string {
return humanize.Bytes(uint64(size))
},
"AttemptStatus": attemptStatus,
},
DefaultArgs: func(ctx context.Context, e *templates.Extra) (templates.Args, error) {
loginURL, err := auth.LoginURL(ctx, e.Request.URL.RequestURI())
if err != nil {
return nil, err
}
logoutURL, err := auth.LogoutURL(ctx, e.Request.URL.RequestURI())
if err != nil {
return nil, err
}
token, err := xsrf.Token(ctx)
if err != nil {
return nil, err
}
return templates.Args{
"ImageVersion": opts.ImageVersion(),
"IsAnonymous": auth.CurrentIdentity(ctx) == identity.AnonymousIdentity,
"User": auth.CurrentUser(ctx),
"LoginURL": loginURL,
"LogoutURL": logoutURL,
"XsrfToken": token,
"Now": startTime(ctx),
"HandlerDuration": func() time.Duration {
return clock.Now(ctx).Sub(startTime(ctx))
},
}, nil
},
}
}
func startTime(c context.Context) time.Time {
ts, ok := c.Value(&pageStartTimeContextKey).(time.Time)
if !ok {
panic(errors.New("impossible; pageStartTimeContextKey is not set"))
}
return ts
}
func configsServer(c context.Context) configpb.ConfigsServer {
return c.Value(&configsServerContextKey).(configpb.ConfigsServer)
}
func attemptStatus(attempt *configpb.ConfigSet_Attempt) string {
if !attempt.Success {
return "failed"
}
for _, msg := range attempt.GetValidationResult().GetMessages() {
if msg.GetSeverity() == cfgcommonpb.ValidationResult_WARNING {
return "warning"
}
}
return "success"
}