blob: a4f903162abd8cb702842d1034492b711c2f0b40 [file] [log] [blame]
// Copyright 2016 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 portal
import (
"context"
"errors"
"fmt"
"html/template"
"sync"
)
// Page controls how some portal section (usually corresponding to a key in
// global settings JSON blob) is displayed and edited in UI.
//
// Packages that wishes to expose UI for managing their settings register a page
// via RegisterPage(...) call during init() time.
type Page interface {
// Title is used in UI to name this page.
Title(c context.Context) (string, error)
// Overview is optional HTML paragraph describing this page.
Overview(c context.Context) (template.HTML, error)
// Fields describes the schema of the settings on the page (if any).
Fields(c context.Context) ([]Field, error)
// Actions is additional list of actions to present on the page.
//
// Each action is essentially a clickable button that triggers a parameterless
// callback that either does some state change or (if marked as NoSideEffects)
// just returns some information that is displayed on a separate page.
Actions(c context.Context) ([]Action, error)
// ReadSettings returns a map "field ID => field value to display".
//
// It is called when rendering the settings page.
ReadSettings(c context.Context) (map[string]string, error)
// WriteSettings saves settings described as a map "field ID => field value".
//
// Only values of editable, not read only fields are passed here. All values
// are also validated using field's validators before this call.
WriteSettings(c context.Context, values map[string]string) error
}
// Field is description of a single UI element of the page.
//
// Its ID acts as a key in map used by ReadSettings\WriteSettings.
type Field struct {
ID string // page unique ID
Title string // human friendly name
Type FieldType // how the field is displayed and behaves
ReadOnly bool // if true, display the field as immutable
Placeholder string // optional placeholder value
Validator func(string) error // optional value validation
Help template.HTML // optional help text
ChoiceVariants []string // valid only for FieldChoice
}
// FieldType describes look and feel of UI field, see the enum below.
type FieldType string
// Note: exact values here are important. They are referenced in the HTML
// template that renders the settings page. See server/portal/*.
const (
FieldText FieldType = "text" // one line of text, editable
FieldChoice FieldType = "choice" // pick one of predefined choices
FieldStatic FieldType = "static" // one line of text, read only
FieldPassword FieldType = "password" // one line of text, editable but obscured
)
// IsEditable returns true for fields that can be edited.
func (f *Field) IsEditable() bool {
return f.Type != FieldStatic && !f.ReadOnly
}
// Action corresponds to a button that triggers a parameterless callback.
type Action struct {
ID string // page-unique ID
Title string // what's displayed on the button
Help template.HTML // optional help text
Confirmation string // optional text for "Are you sure?" confirmation prompt
NoSideEffects bool // if true, the callback just returns some data
// Callback is executed on click on the action button.
//
// Usually it will execute some state change and return the confirmation text
// (along with its title). If NoSideEffects is true, it may just fetch and
// return some data (which is either too big or too costly to fetch on the
// main page).
Callback func(c context.Context) (title string, body template.HTML, err error)
}
// BasePage can be embedded into Page implementers to provide default
// behavior.
type BasePage struct{}
// Title is used in UI to name this portal page.
func (BasePage) Title(c context.Context) (string, error) {
return "Untitled portal page", nil
}
// Overview is optional HTML paragraph describing this portal page.
func (BasePage) Overview(c context.Context) (template.HTML, error) {
return "", nil
}
// Fields describes the schema of the settings on the page (if any).
func (BasePage) Fields(c context.Context) ([]Field, error) {
return nil, nil
}
// Actions is additional list of actions to present on the page.
func (BasePage) Actions(c context.Context) ([]Action, error) {
return nil, nil
}
// ReadSettings returns a map "field ID => field value to display".
func (BasePage) ReadSettings(c context.Context) (map[string]string, error) {
return nil, nil
}
// WriteSettings saves settings described as a map "field ID => field value".
func (BasePage) WriteSettings(c context.Context, values map[string]string) error {
return errors.New("not implemented")
}
// RegisterPage makes exposes UI for a portal page (identified by given
// unique key).
//
// Should be called once when application starts (e.g. from init() of a package
// that defines the page). Panics if such key is already registered.
func RegisterPage(pageKey string, p Page) {
registry.registerPage(pageKey, p)
}
// GetPages returns a map with all registered pages.
func GetPages() map[string]Page {
return registry.getPages()
}
////////////////////////////////////////////////////////////////////////////////
// Internal stuff.
var registry pageRegistry
type pageRegistry struct {
lock sync.RWMutex
pages map[string]Page
}
func (r *pageRegistry) registerPage(pageKey string, p Page) {
r.lock.Lock()
defer r.lock.Unlock()
if r.pages == nil {
r.pages = make(map[string]Page)
}
if existing, _ := r.pages[pageKey]; existing != nil {
panic(fmt.Errorf("portal page for %s is already registered: %T", pageKey, existing))
}
r.pages[pageKey] = p
}
func (r *pageRegistry) getPages() map[string]Page {
r.lock.RLock()
defer r.lock.RUnlock()
cpy := make(map[string]Page, len(r.pages))
for k, v := range r.pages {
cpy[k] = v
}
return cpy
}