go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/portal/page.go (about)

     1  // Copyright 2016 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 portal
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"html/template"
    22  	"sync"
    23  )
    24  
    25  // Page controls how some portal section (usually corresponding to a key in
    26  // global settings JSON blob) is displayed and edited in UI.
    27  //
    28  // Packages that wishes to expose UI for managing their settings register a page
    29  // via RegisterPage(...) call during init() time.
    30  type Page interface {
    31  	// Title is used in UI to name this page.
    32  	Title(c context.Context) (string, error)
    33  
    34  	// Overview is optional HTML paragraph describing this page.
    35  	Overview(c context.Context) (template.HTML, error)
    36  
    37  	// Fields describes the schema of the settings on the page (if any).
    38  	Fields(c context.Context) ([]Field, error)
    39  
    40  	// Actions is additional list of actions to present on the page.
    41  	//
    42  	// Each action is essentially a clickable button that triggers a parameterless
    43  	// callback that either does some state change or (if marked as NoSideEffects)
    44  	// just returns some information that is displayed on a separate page.
    45  	Actions(c context.Context) ([]Action, error)
    46  
    47  	// ReadSettings returns a map "field ID => field value to display".
    48  	//
    49  	// It is called when rendering the settings page.
    50  	ReadSettings(c context.Context) (map[string]string, error)
    51  
    52  	// WriteSettings saves settings described as a map "field ID => field value".
    53  	//
    54  	// Only values of editable, not read only fields are passed here. All values
    55  	// are also validated using field's validators before this call.
    56  	WriteSettings(c context.Context, values map[string]string) error
    57  }
    58  
    59  // Field is description of a single UI element of the page.
    60  //
    61  // Its ID acts as a key in map used by ReadSettings\WriteSettings.
    62  type Field struct {
    63  	ID             string             // page unique ID
    64  	Title          string             // human friendly name
    65  	Type           FieldType          // how the field is displayed and behaves
    66  	ReadOnly       bool               // if true, display the field as immutable
    67  	Placeholder    string             // optional placeholder value
    68  	Validator      func(string) error // optional value validation
    69  	Help           template.HTML      // optional help text
    70  	ChoiceVariants []string           // valid only for FieldChoice
    71  }
    72  
    73  // FieldType describes look and feel of UI field, see the enum below.
    74  type FieldType string
    75  
    76  // Note: exact values here are important. They are referenced in the HTML
    77  // template that renders the settings page. See server/portal/*.
    78  const (
    79  	FieldText     FieldType = "text"     // one line of text, editable
    80  	FieldChoice   FieldType = "choice"   // pick one of predefined choices
    81  	FieldStatic   FieldType = "static"   // one line of text, read only
    82  	FieldPassword FieldType = "password" // one line of text, editable but obscured
    83  )
    84  
    85  // IsEditable returns true for fields that can be edited.
    86  func (f *Field) IsEditable() bool {
    87  	return f.Type != FieldStatic && !f.ReadOnly
    88  }
    89  
    90  // Action corresponds to a button that triggers a parameterless callback.
    91  type Action struct {
    92  	ID            string        // page-unique ID
    93  	Title         string        // what's displayed on the button
    94  	Help          template.HTML // optional help text
    95  	Confirmation  string        // optional text for "Are you sure?" confirmation prompt
    96  	NoSideEffects bool          // if true, the callback just returns some data
    97  
    98  	// Callback is executed on click on the action button.
    99  	//
   100  	// Usually it will execute some state change and return the confirmation text
   101  	// (along with its title). If NoSideEffects is true, it may just fetch and
   102  	// return some data (which is either too big or too costly to fetch on the
   103  	// main page).
   104  	Callback func(c context.Context) (title string, body template.HTML, err error)
   105  }
   106  
   107  // BasePage can be embedded into Page implementers to provide default
   108  // behavior.
   109  type BasePage struct{}
   110  
   111  // Title is used in UI to name this portal page.
   112  func (BasePage) Title(c context.Context) (string, error) {
   113  	return "Untitled portal page", nil
   114  }
   115  
   116  // Overview is optional HTML paragraph describing this portal page.
   117  func (BasePage) Overview(c context.Context) (template.HTML, error) {
   118  	return "", nil
   119  }
   120  
   121  // Fields describes the schema of the settings on the page (if any).
   122  func (BasePage) Fields(c context.Context) ([]Field, error) {
   123  	return nil, nil
   124  }
   125  
   126  // Actions is additional list of actions to present on the page.
   127  func (BasePage) Actions(c context.Context) ([]Action, error) {
   128  	return nil, nil
   129  }
   130  
   131  // ReadSettings returns a map "field ID => field value to display".
   132  func (BasePage) ReadSettings(c context.Context) (map[string]string, error) {
   133  	return nil, nil
   134  }
   135  
   136  // WriteSettings saves settings described as a map "field ID => field value".
   137  func (BasePage) WriteSettings(c context.Context, values map[string]string) error {
   138  	return errors.New("not implemented")
   139  }
   140  
   141  // RegisterPage makes exposes UI for a portal page (identified by given
   142  // unique key).
   143  //
   144  // Should be called once when application starts (e.g. from init() of a package
   145  // that defines the page). Panics if such key is already registered.
   146  func RegisterPage(pageKey string, p Page) {
   147  	registry.registerPage(pageKey, p)
   148  }
   149  
   150  // GetPages returns a map with all registered pages.
   151  func GetPages() map[string]Page {
   152  	return registry.getPages()
   153  }
   154  
   155  ////////////////////////////////////////////////////////////////////////////////
   156  // Internal stuff.
   157  
   158  var registry pageRegistry
   159  
   160  type pageRegistry struct {
   161  	lock  sync.RWMutex
   162  	pages map[string]Page
   163  }
   164  
   165  func (r *pageRegistry) registerPage(pageKey string, p Page) {
   166  	r.lock.Lock()
   167  	defer r.lock.Unlock()
   168  	if r.pages == nil {
   169  		r.pages = make(map[string]Page)
   170  	}
   171  	if existing, _ := r.pages[pageKey]; existing != nil {
   172  		panic(fmt.Errorf("portal page for %s is already registered: %T", pageKey, existing))
   173  	}
   174  	r.pages[pageKey] = p
   175  }
   176  
   177  func (r *pageRegistry) getPages() map[string]Page {
   178  	r.lock.RLock()
   179  	defer r.lock.RUnlock()
   180  	cpy := make(map[string]Page, len(r.pages))
   181  	for k, v := range r.pages {
   182  		cpy[k] = v
   183  	}
   184  	return cpy
   185  }