Skip to content

Commit

Permalink
internal/gaby: refactor templates and pages
Browse files Browse the repository at this point in the history
Introduce a Go type, [CommonPage], which holds data common to
many Gaby pages, and define common templates on this type.

The goals are 1) to reduce duplication in template files and 2) to move as
much content as possible out of templates and into Go code.

Updates #55

Change-Id: I882ee4b2ba097de05c6466fb08f10c1bfe79393a
Reviewed-on: https://go-review.googlesource.com/c/oscar/+/626535
Reviewed-by: Jonathan Amsterdam <[email protected]>
LUCI-TryBot-Result: Go LUCI <[email protected]>
  • Loading branch information
tatianab committed Nov 19, 2024
1 parent a5097b3 commit 918aa14
Show file tree
Hide file tree
Showing 22 changed files with 1,004 additions and 654 deletions.
30 changes: 24 additions & 6 deletions internal/gaby/actionlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package main

import (
"bytes"
"encoding/json"
"errors"
"fmt"
Expand All @@ -19,10 +18,10 @@ import (
"golang.org/x/oscar/internal/storage"
)

var _ page = actionLogPage{}

// actionLogPage is the data for the action log HTML template.
type actionLogPage struct {
CommonPage

Start, End endpoint
StartTime, EndTime string // formatted times that the endpoints describe
Entries []*actions.Entry
Expand Down Expand Up @@ -79,11 +78,30 @@ func (g *Gaby) doActionLog(r *http.Request) (content []byte, status int, err err
page.End.Radio = "fixed"
}

var buf bytes.Buffer
if err := actionLogPageTmpl.Execute(&buf, page); err != nil {
page.setCommonPage()

b, err := Exec(actionLogPageTmpl, &page)
if err != nil {
return nil, http.StatusInternalServerError, err
}
return buf.Bytes(), http.StatusOK, nil
return b, http.StatusOK, nil
}

func (p *actionLogPage) setCommonPage() {
p.CommonPage = *p.toCommonPage()
}

func (p *actionLogPage) toCommonPage() *CommonPage {
return &CommonPage{
ID: actionlogID,
Description: "Browse actions taken by Oscar.",
Form: Form{
// Unset because the action log page defines its form inputs
// directly in an HTML template.
Inputs: nil,
SubmitText: "display",
},
}
}

// formValues populates an endpoint from the values in the form.
Expand Down
8 changes: 4 additions & 4 deletions internal/gaby/actionlog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package main

import (
"bytes"
"context"
"strings"
"testing"
Expand Down Expand Up @@ -95,7 +94,6 @@ func TestTimes(t *testing.T) {
}

func TestActionTemplate(t *testing.T) {
var buf bytes.Buffer
page := actionLogPage{
Start: endpoint{DurNum: "3", DurUnit: "days"},
StartTime: "whatevs",
Expand All @@ -107,10 +105,12 @@ func TestActionTemplate(t *testing.T) {
},
},
}
if err := actionLogPageTmpl.Execute(&buf, page); err != nil {
page.setCommonPage()
b, err := Exec(actionLogPageTmpl, &page)
if err != nil {
t.Fatal(err)
}
got := buf.String()
got := string(b)
wants := []string{
`<option value="days" selected>days</option>`,
`Project`,
Expand Down
138 changes: 138 additions & 0 deletions internal/gaby/common_page.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
"bytes"

"github.com/google/safehtml"
"github.com/google/safehtml/template"
)

// Exec executes the given template on the page.
func Exec(tmpl *template.Template, p page) ([]byte, error) {
var buf bytes.Buffer
if err := tmpl.Execute(&buf, p); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

// page is a Gaby webpage containing a [CommonPage].
// Any struct that embeds a [CommonPage] implements this interface.
type page interface {
// do not directly define [isCommonPage].
isCommonPage()
}

// A CommonPage is a partial representation of a Gaby web page,
// used to store data that is common to many pages.
// The templates in tmpl/common.tmpl are defined on this type.
type CommonPage struct {
// The ID of the page.
ID pageID
// A plain text description of the webpage.
Description string
// A list of additional stylesheets to use for this webpage.
// "/static/style.css" and [pageID.CSS] are always included
// without needing to be listed here.
Styles []safeURL
// The input form.
Form Form
}

// Implements [page.isCommonPage].
func (*CommonPage) isCommonPage() {}

// A Form is a representation of an HTML form.
type Form struct {
// (Optional) Description and/or general tips for filling out the form.
Description string
// The text to display on the form's submit button.
SubmitText string
// The form's inputs.
Inputs []FormInput
}

// A FormInput represents an input (or a group of inputs)
// to an HTML form.
type FormInput struct {
Label string // display text
Type string // type to display to the user (for tips section)
Description string // description of the input and its usage (for tips section)

Name safeID // HTML "name"
Required bool // whether the input is required

// Additional data, based on the type of form input.
Typed typedInput
}

type typedInput interface {
InputType() string
}

// TextInput is a an HTML "text" input.
type TextInput struct {
ID safeID // HTML "id"
Value string // HTML "value"
}

// Implments [typedInput.InputType].
func (TextInput) InputType() string {
return "text"
}

// RadioInput is a collection of HTML "radio" inputs.
type RadioInput struct {
Choices []RadioChoice
}

// Implements [typedInput.InputType].
func (RadioInput) InputType() string {
return "radio"
}

// RadioChoice is a single HTML "radio" input.
type RadioChoice struct {
Label string // display text
ID safeID // HTML "id"
Value string // HTML "value"
Checked bool // whether the button should be checked
}

type pageID string

func (p pageID) Endpoint() string {
return "/" + string(p)
}

func (p pageID) Title() string {
if t, ok := titles[p]; ok {
return t
}
return string(p)
}

func (p pageID) CSS() safeURL {
const cssFmt = "/static/%{p}.css"
u, err := safehtml.TrustedResourceURLFormatFromConstant(cssFmt, map[string]string{"p": string(p)})
if err != nil {
panic(err)
}
return u
}

// Shorthands for safehtml types.
type (
safeID = safehtml.Identifier
safeURL = safehtml.TrustedResourceURL
)

// Shorthands for safehtml functions.
var (
toSafeID = safehtml.IdentifierFromConstant
toSafeURL = safehtml.TrustedResourceURLFromConstant
)
92 changes: 79 additions & 13 deletions internal/gaby/dbview.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ import (
"rsc.io/ordered"
)

var _ page = dbviewPage{}

// dbviewPage holds the fields needed to display a view of the database.
type dbviewPage struct {
Form dbviewForm // the raw form inputs
CommonPage

Params dbviewParams // the raw parameters
Result *dbviewResult
Error error // if non-nil, the error to display instead of the result
}

type dbviewForm struct {
Start, End string // comma-separated lists; see parseOredered for details
Limit int
type dbviewParams struct {
Start, End string // comma-separated lists; see [parseOrdered] for details
Limit string // the maximum number of values to display
}

type dbviewResult struct {
Expand All @@ -46,17 +46,18 @@ func (g *Gaby) handleDBview(w http.ResponseWriter, r *http.Request) {
}

// populateDBviewPage returns the contents of the dbView page.
func (g *Gaby) populateDBviewPage(r *http.Request) dbviewPage {
limit := parseInt(r.FormValue("limit"), 100)
p := dbviewPage{
Form: dbviewForm{
func (g *Gaby) populateDBviewPage(r *http.Request) *dbviewPage {
p := &dbviewPage{
Params: dbviewParams{
Start: r.FormValue("start"),
End: r.FormValue("end"),
Limit: limit,
Limit: formValue(r, "limit", "100"),
},
}
start := parseOrdered(p.Form.Start)
end := parseOrdered(p.Form.End)
p.setCommonPage()
limit := parseInt(p.Params.Limit, 100)
start := parseOrdered(p.Params.Start)
end := parseOrdered(p.Params.End)
g.slog.Info("calling dbview", "limit", limit)
res, err := g.dbview(start, end, limit)
g.slog.Info("done")
Expand All @@ -68,6 +69,19 @@ func (g *Gaby) populateDBviewPage(r *http.Request) dbviewPage {
return p
}

func (p *dbviewPage) setCommonPage() {
p.CommonPage = CommonPage{
ID: dbviewID,
Description: "View the database contents.",
Form: Form{
Description: `Provide one key to get a single value, or two to get a range.
Keys are comma-separated lists of strings, integers, "inf" or "-inf".`,
Inputs: p.Params.inputs(),
SubmitText: "Show",
},
}
}

func (g *Gaby) dbview(start, end []byte, limit int) (*dbviewResult, error) {
if len(start) == 0 && len(end) > 0 {
return nil, errors.New("missing start key")
Expand Down Expand Up @@ -144,3 +158,55 @@ func parseInt(s string, defaultValue int) int {
}
return defaultValue
}

// formValue returns the form value for the key, or defaultValue
// if the form value is empty.
func formValue(r *http.Request, key string, defaultValue string) string {
if v := r.FormValue(key); v != "" {
return v
}
return defaultValue
}

var (
safeStart = toSafeID("start")
safeEnd = toSafeID("end")
)

func (pm *dbviewParams) inputs() []FormInput {
return []FormInput{
{
Label: "Get",
Type: "db key",
Description: "the starting db key",
Name: safeStart,
Required: true,
Typed: TextInput{
ID: safeStart,
Value: pm.Start,
},
},
{
Label: "To",
Type: "db key",
Description: "the ending db key",
Name: safeEnd,
// optional
Typed: TextInput{
ID: safeEnd,
Value: pm.End,
},
},
{
Label: "Limit",
Type: "int",
Description: "the maximum number of values to display (default: 100)",
Name: safeLimit,
Required: true,
Typed: TextInput{
ID: safeLimit,
Value: pm.Limit,
},
},
}
}
16 changes: 10 additions & 6 deletions internal/gaby/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,30 +493,34 @@ func (g *Gaby) newServer(report func(error)) *http.ServeMux {
}
})

get := func(p pageID) string {
return "GET " + p.Endpoint()
}

// /search: display a form for vector similarity search.
// /search?q=...: perform a search using the value of q as input.
mux.HandleFunc("GET /search", g.handleSearch)
mux.HandleFunc(get(searchID), g.handleSearch)

// /overview: display a form for LLM-generated overviews of data.
// /overview?q=...: generate an overview using the value of q as input.
mux.HandleFunc("GET /overview", g.handleOverview)
mux.HandleFunc(get(overviewID), g.handleOverview)

// /rules: display a form for entering an issue to check for rule violations.
// /rules?q=...: generate a list of violated rules for issue q.
mux.HandleFunc("GET /rules", g.handleRules)
mux.HandleFunc(get(rulesID), g.handleRules)

// /api/search: perform a vector similarity search.
// POST because the arguments to the request are in the body.
mux.HandleFunc("POST /api/search", g.handleSearchAPI)

// /actionlog: display action log
mux.HandleFunc("GET /actionlog", g.handleActionLog)
mux.HandleFunc(get(actionlogID), g.handleActionLog)

// /reviews: display review dashboard
mux.HandleFunc("GET /reviews", g.handleReviewDashboard)
mux.HandleFunc(get(reviewsID), g.handleReviewDashboard)

// /dbview: view parts of the database
mux.HandleFunc("GET /dbview", g.handleDBview)
mux.HandleFunc(get(dbviewID), g.handleDBview)

return mux
}
Expand Down
Loading

0 comments on commit 918aa14

Please sign in to comment.