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  }