/
page.go
185 lines (157 loc) · 6.39 KB
/
page.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// 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, who, why 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, who, why 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
}