github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/plugin/plugin_ui.go (about) 1 package plugin 2 3 import ( 4 "fmt" 5 "html/template" 6 "net/http" 7 "path/filepath" 8 "sort" 9 10 "github.com/evergreen-ci/evergreen" 11 "github.com/evergreen-ci/evergreen/model" 12 "github.com/evergreen-ci/evergreen/model/build" 13 "github.com/evergreen-ci/evergreen/model/patch" 14 "github.com/evergreen-ci/evergreen/model/task" 15 "github.com/evergreen-ci/evergreen/model/user" 16 "github.com/evergreen-ci/evergreen/model/version" 17 "github.com/gorilla/context" 18 "github.com/pkg/errors" 19 ) 20 21 // PanelConfig stores all UI-related plugin hooks 22 type PanelConfig struct { 23 // Handler is an http.Handler which receives plugin-specific HTTP requests from the UI 24 Handler http.Handler 25 26 // Panels is an array of UIPanels to inject into task, version, 27 // and build pages 28 Panels []UIPanel 29 } 30 31 // PageScope is a type for setting the page a panel appears on 32 type PageScope string 33 34 // pagePosition is a private type for setting where on a page a panel appears 35 type pagePosition string 36 37 const ( 38 // These PageScope constants determine which page a panel hooks into 39 TaskPage PageScope = "task" 40 BuildPage PageScope = "build" 41 VersionPage PageScope = "version" 42 43 // These pagePosition constants determine where on a page a panel is 44 // injected. If no position is given, it defaults to PageCenter 45 PageLeft pagePosition = "Left" 46 PageRight pagePosition = "Right" 47 PageCenter pagePosition = "Center" 48 ) 49 50 type pluginContext int 51 type pluginUser int 52 53 const pluginContextKey pluginContext = 0 54 const pluginUserKey pluginUser = 0 55 56 // MustHaveContext loads a UIContext from the http.Request. It panics 57 // if the context is not set. 58 func MustHaveContext(request *http.Request) UIContext { 59 if c := context.Get(request, pluginContextKey); c != nil { 60 return c.(UIContext) 61 } 62 panic("no UI context found") 63 } 64 65 // SetUser sets the user for the request context. This is a helper for UI middleware. 66 func SetUser(u *user.DBUser, r *http.Request) { 67 context.Set(r, pluginUserKey, u) 68 } 69 70 // GetUser fetches the user, if it exists, from the request context. 71 func GetUser(r *http.Request) *user.DBUser { 72 if rv := context.Get(r, pluginUserKey); rv != nil { 73 return rv.(*user.DBUser) 74 } 75 return nil 76 } 77 78 // UIDataFunction is a function which is called to populate panels 79 // which are injected into Task/Build/Version pages at runtime. 80 type UIDataFunction func(context UIContext) (interface{}, error) 81 82 // UIPage represents the information to be sent over to the ui server 83 // in order to render a page for an app level plugin. 84 // TemplatePath is the relative path to the template from the template root of the plugin. 85 // Data represents the data to send over. 86 type UIPage struct { 87 TemplatePath string 88 DataFunc UIDataFunction 89 } 90 91 // UIPanel is a type for storing all the configuration to properly 92 // display one panel in one page of the UI. 93 type UIPanel struct { 94 // Page is which page the panel appears on 95 Page PageScope 96 97 // Includes is a list of HTML tags to inject into the head of 98 // the page. These are meant to be links to css and js code hosted 99 // in the plugin's static web root 100 Includes []template.HTML 101 102 // PanelHTML is the HTML definition of the panel. Best practices dictate 103 // using AngularJS to load up the html as a partial hosted by the plugin 104 PanelHTML template.HTML 105 106 // DataFunc is a function to populate plugin data injected into the js 107 // of the page the panel is on. The function takes the page request as 108 // an argument, and returns data (must be json-serializeable!) or an error 109 DataFunc UIDataFunction 110 // Position is the side of the page the panel appears in 111 Position pagePosition 112 } 113 114 // UIContext stores all relevant models for a plugin page. 115 type UIContext struct { 116 Settings evergreen.Settings 117 User *user.DBUser 118 Task *task.Task 119 Build *build.Build 120 Version *version.Version 121 Patch *patch.Patch 122 Project *model.Project 123 ProjectRef *model.ProjectRef 124 Request *http.Request 125 } 126 127 // PanelLayout tells the view renderer what panel HTML data to inject and where 128 // on the page to inject it. 129 type PanelLayout struct { 130 Left []template.HTML 131 Right []template.HTML 132 Center []template.HTML 133 } 134 135 // PanelManager is the manager the UI server uses to register and load 136 // plugin UI information efficiently. 137 type PanelManager interface { 138 RegisterPlugins([]UIPlugin) error 139 Includes(PageScope) ([]template.HTML, error) 140 Panels(PageScope) (PanelLayout, error) 141 UIData(UIContext, PageScope) (map[string]interface{}, error) 142 GetAppPlugins() []AppUIPlugin 143 } 144 145 // private type for sorting alphabetically, 146 // holds a pairing of plugin name and HTML 147 type pluginTemplatePair struct { 148 Name string 149 Template template.HTML 150 } 151 152 // private type for sorting methods 153 type pairsByLexicographicalOrder []pluginTemplatePair 154 155 func (a pairsByLexicographicalOrder) Len() int { 156 return len(a) 157 } 158 func (a pairsByLexicographicalOrder) Swap(i, j int) { 159 a[i], a[j] = a[j], a[i] 160 } 161 func (a pairsByLexicographicalOrder) Less(i, j int) bool { 162 return a[i].Name < a[j].Name 163 } 164 165 // sortAndExtractHTML takes a list of pairings of <plugin name, html data>, 166 // sorts them by plugin name, and returns a list of the just 167 // the html data 168 func sortAndExtractHTML(pairs []pluginTemplatePair) []template.HTML { 169 var tplList []template.HTML 170 sort.Stable(pairsByLexicographicalOrder(pairs)) 171 for _, pair := range pairs { 172 tplList = append(tplList, pair.Template) 173 } 174 return tplList 175 } 176 177 // SimplePanelManager is a basic implementation of a plugin panel manager. 178 type SimplePanelManager struct { 179 includes map[PageScope][]template.HTML 180 panelHTML map[PageScope]PanelLayout 181 uiDataFuncs map[PageScope]map[string]UIDataFunction 182 appPlugins []AppUIPlugin 183 } 184 185 // RegisterPlugins takes an array of plugins and registers them with the 186 // manager. After this step is done, the other manager functions may be used. 187 func (self *SimplePanelManager) RegisterPlugins(plugins []UIPlugin) error { 188 //initialize temporary maps 189 registered := map[string]bool{} 190 includesWithPair := map[PageScope][]pluginTemplatePair{} 191 panelHTMLWithPair := map[PageScope]map[pagePosition][]pluginTemplatePair{ 192 TaskPage: {}, 193 BuildPage: {}, 194 VersionPage: {}, 195 } 196 dataFuncs := map[PageScope]map[string]UIDataFunction{ 197 TaskPage: {}, 198 BuildPage: {}, 199 VersionPage: {}, 200 } 201 202 appPluginNames := []AppUIPlugin{} 203 for _, p := range plugins { 204 // don't register plugins twice 205 if registered[p.Name()] { 206 return errors.Errorf("plugin '%v' already registered", p.Name()) 207 } 208 // check if a plugin is an app level plugin first 209 if appPlugin, ok := p.(AppUIPlugin); ok { 210 appPluginNames = append(appPluginNames, appPlugin) 211 } 212 213 if uiConf, err := p.GetPanelConfig(); uiConf != nil && err == nil { 214 for _, panel := range uiConf.Panels { 215 216 // register all includes to their proper scope 217 for _, include := range panel.Includes { 218 includesWithPair[panel.Page] = append( 219 includesWithPair[panel.Page], 220 pluginTemplatePair{p.Name(), include}, 221 ) 222 } 223 224 // register all panels to their proper scope and position 225 if panel.Page == "" { 226 return errors.New("plugin '%v': cannot register ui panel without a Page") 227 } 228 if panel.Position == "" { 229 panel.Position = PageCenter // Default to center 230 } 231 panelHTMLWithPair[panel.Page][panel.Position] = append( 232 panelHTMLWithPair[panel.Page][panel.Position], 233 pluginTemplatePair{p.Name(), panel.PanelHTML}, 234 ) 235 236 // register all data functions to their proper scope, if they exist 237 // Note: only one function can be registered per plugin per page 238 if dataFuncs[panel.Page][p.Name()] == nil { 239 if panel.DataFunc != nil { 240 dataFuncs[panel.Page][p.Name()] = panel.DataFunc 241 } 242 } else { 243 if panel.DataFunc != nil { 244 return errors.Errorf( 245 "a data function is already registered for plugin %v on %v page", 246 p.Name(), panel.Page) 247 } 248 } 249 } 250 } else if err != nil { 251 return errors.Wrapf(err, "GetPanelConfig for plugin '%v' returned an error", p.Name()) 252 } 253 254 registered[p.Name()] = true 255 } 256 257 self.appPlugins = appPluginNames 258 259 // sort registered plugins by name and cache their HTML 260 self.includes = map[PageScope][]template.HTML{ 261 TaskPage: sortAndExtractHTML(includesWithPair[TaskPage]), 262 BuildPage: sortAndExtractHTML(includesWithPair[BuildPage]), 263 VersionPage: sortAndExtractHTML(includesWithPair[VersionPage]), 264 } 265 self.panelHTML = map[PageScope]PanelLayout{ 266 TaskPage: { 267 Left: sortAndExtractHTML(panelHTMLWithPair[TaskPage][PageLeft]), 268 Right: sortAndExtractHTML(panelHTMLWithPair[TaskPage][PageRight]), 269 Center: sortAndExtractHTML(panelHTMLWithPair[TaskPage][PageCenter]), 270 }, 271 BuildPage: { 272 Left: sortAndExtractHTML(panelHTMLWithPair[BuildPage][PageLeft]), 273 Right: sortAndExtractHTML(panelHTMLWithPair[BuildPage][PageRight]), 274 Center: sortAndExtractHTML(panelHTMLWithPair[BuildPage][PageCenter]), 275 }, 276 VersionPage: { 277 Left: sortAndExtractHTML(panelHTMLWithPair[VersionPage][PageLeft]), 278 Right: sortAndExtractHTML(panelHTMLWithPair[VersionPage][PageRight]), 279 Center: sortAndExtractHTML(panelHTMLWithPair[VersionPage][PageCenter]), 280 }, 281 } 282 self.uiDataFuncs = dataFuncs 283 284 return nil 285 } 286 287 // Includes returns a properly-ordered list of html tags to inject into the 288 // head of the view for the given page. 289 func (self *SimplePanelManager) Includes(page PageScope) ([]template.HTML, error) { 290 return self.includes[page], nil 291 } 292 293 // Panels returns a PanelLayout for the view renderer to inject panels into 294 // the given page. 295 func (self *SimplePanelManager) Panels(page PageScope) (PanelLayout, error) { 296 return self.panelHTML[page], nil 297 } 298 299 func (self *SimplePanelManager) GetAppPlugins() []AppUIPlugin { 300 return self.appPlugins 301 } 302 303 // UIData returns a map of plugin name -> data for inclusion 304 // in the view's javascript. 305 func (self *SimplePanelManager) UIData(context UIContext, page PageScope) (map[string]interface{}, error) { 306 pluginUIData := map[string]interface{}{} 307 errs := &UIDataFunctionError{} 308 for plName, dataFunc := range self.uiDataFuncs[page] { 309 // run the data function, catching all sorts of errors 310 plData, err := func() (data interface{}, err error) { 311 defer func() { 312 if r := recover(); r != nil { 313 err = errors.Errorf("plugin function panicked: %v", r) 314 } 315 }() 316 data, err = dataFunc(context) 317 return 318 }() 319 if err != nil { 320 // append error to error list and continue 321 plData = nil 322 errs.AppendError(plName, err) 323 } 324 pluginUIData[plName] = plData 325 } 326 if errs.HasErrors() { 327 return pluginUIData, errs 328 } 329 return pluginUIData, nil 330 } 331 332 // UIDataFunctionError is a special error type for data function processing 333 // which can record and aggregate multiple error messages. 334 type UIDataFunctionError []error 335 336 // AppendError adds an error onto the array of data function errors. 337 func (errs *UIDataFunctionError) AppendError(name string, err error) { 338 *errs = append(*errs, errors.Errorf("{'%v': %v}", name, err)) 339 } 340 341 // HasErrors returns a boolean representing if the UIDataFunctionError 342 // contains any errors. 343 func (errs *UIDataFunctionError) HasErrors() bool { 344 return len(*errs) > 0 345 } 346 347 // Error returns a string aggregating the stored error messages. 348 // Implements the error interface. 349 func (errs *UIDataFunctionError) Error() string { 350 return fmt.Sprintf( 351 "encountered errors processing UI data functions: %v", 352 *errs, 353 ) 354 } 355 356 func TemplateRoot(name string) string { 357 return filepath.Join(evergreen.FindEvergreenHome(), "service", "plugins", name, "templates") 358 }