blob: 34d7a96fc480e97587215674a64abdac4aa8a2dc [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 settings
import (
"errors"
"fmt"
"html/template"
"sync"
"golang.org/x/net/context"
)
// UIPage controls how some settings 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 RegisterUIPage(...) call during init() time.
type UIPage interface {
// Title is used in UI to name this settings page.
Title(c context.Context) (string, error)
// Overview is optional HTML paragraph describing this settings page.
Overview(c context.Context) (template.HTML, error)
// Fields describes the schema of this settings page.
Fields(c context.Context) ([]UIField, 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".
//
// All values are validated using field validators first.
WriteSettings(c context.Context, values map[string]string, who, why string) error
}
// UIField is description of a single UI element of the settings page.
//
// Its ID acts as a key in map used by ReadSettings\WriteSettings.
type UIField struct {
ID string // page unique ID
Title string // human friendly name
Type UIFieldType // how the field is displayed and behaves
Placeholder string // optional placeholder value
Validator func(string) error // optional value validation
Help template.HTML // optional help text
ChoiceVariants []string // valid only for UIFieldChoice
}
// UIFieldType describes look and feel of UI field, see the enum below.
type UIFieldType string
// Note: exact values here are important. They are referenced in the HTML
// template that renders the settings page. See server/settings/admin/*.
const (
UIFieldText UIFieldType = "text" // one line of text, editable
UIFieldChoice UIFieldType = "choice" // pick one of predefined choices
UIFieldStatic UIFieldType = "static" // one line of text, read only
)
// IsEditable returns true for fields that can be edited.
func (f UIFieldType) IsEditable() bool {
return f != UIFieldStatic
}
// BaseUIPage can be embedded into UIPage implementers to provide default
// behavior.
type BaseUIPage struct{}
// Title is used in UI to name this settings block.
func (BaseUIPage) Title(c context.Context) (string, error) {
return "Untitled settings", nil
}
// Overview is optional HTML paragraph describing this settings block.
func (BaseUIPage) Overview(c context.Context) (template.HTML, error) {
return "", nil
}
// Fields describes the schema of the config page.
func (BaseUIPage) Fields(c context.Context) ([]UIField, error) {
return nil, errors.New("not implemented")
}
// ReadSettings returns a map "field ID => field value to display".
func (BaseUIPage) ReadSettings(c context.Context) (map[string]string, error) {
return nil, errors.New("not implemented")
}
// WriteSettings saves settings described as a map "field ID => field value".
func (BaseUIPage) WriteSettings(c context.Context, values map[string]string, who, why string) error {
return errors.New("not implemented")
}
// RegisterUIPage makes exposes UI for a block of settings (identified by given
// unique key).
//
// Should be called once when application starts (e.g. from init() of a package
// that defines the settings). Panics if such key is already registered.
func RegisterUIPage(settingsKey string, p UIPage) {
registry.registerUIPage(settingsKey, p)
}
// GetUIPages returns a map with all registered pages.
func GetUIPages() map[string]UIPage {
return registry.getUIPages()
}
////////////////////////////////////////////////////////////////////////////////
// Internal stuff.
var registry pageRegistry
type pageRegistry struct {
lock sync.RWMutex
pages map[string]UIPage
}
func (r *pageRegistry) registerUIPage(settingsKey string, p UIPage) {
r.lock.Lock()
defer r.lock.Unlock()
if r.pages == nil {
r.pages = make(map[string]UIPage)
}
if existing, _ := r.pages[settingsKey]; existing != nil {
panic(fmt.Errorf("settings UI page for %s is already registered: %T", settingsKey, existing))
}
r.pages[settingsKey] = p
}
func (r *pageRegistry) getUIPages() map[string]UIPage {
r.lock.RLock()
defer r.lock.RUnlock()
cpy := make(map[string]UIPage, len(r.pages))
for k, v := range r.pages {
cpy[k] = v
}
return cpy
}