If you hit a generic spotlight dashboard or module page, this is what happens.
What does "module" mean to spotlight?
-
A "dashboard page" shows a bunch of graphs -- each of these is a module.
-
A "module page" is a page with a graph and a table, which are just different representations of the same data.
After getting a request, you'll start off in appBuilder.js
, which lists all the routes. The explicitly named routes are quite straightforward (/performance/about
, etc), but they represent a minority of pages in the application.
Most of the application is either
- dashboard pages: pages with lots of graphs
- module pages: pages with one graph and a table
There’s one route that handles both of these, and it fobs you off to process_request.js
.
At a high level, process_request.js
is doing two things.
- It has a
setup()
function, which builds and returns a stagecraft API client object fromget_dashboard_and_render.js
- There’s a
renderContent()
function which (because in JavaScript, functions are first-class citizens) gets passed further and further down potentially endless fractals of nested objects as a callback
The setup function is called first.
Mostly what this code is doing is creating the stagecraft API client. It passes in some data from the URL (needed to call the Stagecraft API) and sets up event listeners on the API client. Its most important function, get_dashboard_and_render()
, takes renderContent()
(from the previous file) as a callback and runs it once the API client has been sync
ed. (sync
is a Backbone convention -- more on this later).
The StagecraftApiClient
is an object that makes the initial async call to the API (using fetch()
, another Backbone convention) and then parses the JSON response into objects the application is expecting. As part of this, it assigns a controller to each module.
(So a grouped_timeseries
module will be assigned a GroupedTimeseriesModule
controller. Controllers (also confusingly called "modules") are defined in server/controller_map.js
.)
Once the API client has been initialised, then setup()
is finished. At this point, we wait for the API client to trigger an event: either it will sync
(likely) or it will error
(unlikely).
Backbone follows an event-driven paradigm: ie, instead of writing procedural code that executes sequentially until the end of the file, you react to events in the DOM. So, for example, you could set up a "start" button and wait for a "click" event before starting anything.
Since Backbone is event-driven, there’s a point at which just following the code is misleading, because it will just stop somewhere and start again somewhere else. There are also several points in the code where events are triggered programmatically (ie, you can just call 'button.click()' to force an event.)
The general idea here is that spotlight sets up what it can set up with just the URL of the request, and then it waits for the sync
event, triggered once a response has been successfully returned from the API, before doing any more.
There are two important sync
s that need to happen.
- First, get a JSON file which defines the modules on the page. (ie, a dashboard might have 6 modules: 3 big numbers at the top of the page, 2 line graphs, and 1 bar graph)
- Secondly, for each module, the a separate call has to be made (not using the API client) to get the data for the graph
Once the .setup()
function is completed then we wait for the first of these: for the stagecraft API client to return a list of modules.
Once the data from the API is sync
ed, it triggers the event listener in this file. This function then calls renderContent()
, which was defined in process_request.js
.
In renderContent()
, the model
variable at this point is the API client which now has a bunch of config data about the page we're loading and a bunch of application defaults set by PageConfig
.
If you were looking to add a new generic parameter to the application, a good place to put it would be in the commonConfig
in page_config.js
.
Our model
also has a controller, which contains the logic for the particular page it's loading. This is set by the stagecraft API client. (For dashboard pages and module pages, this controller will always be the DashboardController
)
Once we have the initial JSON response, we know
- what the main controller is (ie, a dashboard controller)
- what all of the submodules are (ie, each of the graphs) as well as each of their controllers
A ready
event listener is set up on the controller, which doesn't do anything right now. This is the code that actually sends the response back to the browser, but at this point we haven't built the HTML yet.
Finally, near the bottom, there's a controller.render()
call. This is pretty much what builds the page. render()
is called for whichever controller was set by the API client.
This isn't functionally important, but should probably be mentioned. Spotlight has wrappers that start timers and then call your business logic as callback methods. controller.render()
is being timed. If you’re debugging and you end up in a timer, look for the callback and keep going from there. (ie, cachedFn.apply(object, arguments);
)
render()
calls this.renderDashboard()
and then passes the Controller.prototype.render
function through as a callback. This means the generic render()
method attached to the Controller base class (app/extensions/controllers/controller.js
) will be called somewhere down the line.
renderDashboard()
pretty much immediately just calls this.renderModules()
. The callback
it passes through is the Controller.prototype.render()
function.
renderModules()
sets the base controller (ie, the dashboard) to listen for the loaded
event, which will kick off the render method of the highest-level controller. It then it runs map
over all the submodules, adding a ready
event listener to each of them, and then calling render()
on each of them individually.
render()
is also a function in this file. Since all of the submodule controllers are also instances of this base Controller prototype, calling render()
on them will run the function in this file. render()
sets up one-time event listeners for both reset
and error
events, and then it calls collection.fetch()
. In practice, we expect the reset
to be triggered.
fetch()
is a Backbone convention that connects to some external source to get some data. Calling fetch()
should be possible on models or collections of models, after which some data will be returned and an event will be triggered. This is a pretty key to how Backbone works.
What is happening here is that each of the modules contains the metadata for a graph (ie, its name and description and what kind of graph it is), but they still need to actually populate the graph with values. So calling collection.fetch()
should return all the actual data that we can then use to build the graph and the data table.
fetch()
eventually calls sync()
which is defined in this file. It escapes the returned values but otherwise it is just using the Backbone default logic. Interestingly, this fetch()
doesn't use the stagecraft API client.
- Once the data that populates each of the graphs is returned successfully, a
reset
event is triggered - Once the
reset
event is triggered, then we callrenderView()
renderView()
:- creates a new
View
class based on the module - and then calls
render()
on this new View
And here's where we finally start building our HTML -- effectively a global variable we can keep referring to and populating.
The base (outermost) View will assign itself a template (this.$el.html(this.template(context));
), which is a page that has lots of empty elements.
<div class=“visualisation-inner”></div>
<div class=“visualisation-table”></div>
Once this is set up, it calls this.renderSubviews()
.
renderSubviews()
cycles through the submodules contained by this root View and tries to build them. Each submodule has a classname or id as an identifier, so for each one, we will look in the current HTML for the id or classname to latch onto, and then create a new View(options)
which is immediately rendered, effectively building the HTML that describes what they are inside their (initially empty) container element.
app/extensions/views/table.js
(for example)
As an example, if our submodule is a graph, then calling the new View
in the base view file will create a new TableView
, and then view.render()
will call the the render()
method of the TableView. This is probably what were were looking for in the first place.
Once we're here, we're finally in the guts of the page we're trying to build and we can start making changes to the output of the page.