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 }