-
Notifications
You must be signed in to change notification settings - Fork 9
Topcat Javascript Overview
Brian Ritchie, Nov 2019
This is very much an overview of the Topcat Javascript code in that I don't intend to go into great detail. (Well, most of the time...) It seems unlikely that anyone will want to make any significant changes; but it may be useful to know where to look if something starts to go wrong.
I will try to give a brief description of each module, and outline where and (if you're lucky) how it is used by the rest of the code. An alternative approach would be to start with app.js and work downwards, but I will try a per-module description first.
The modules are split into type: controllers, directives, filters, services (all under yo/app/scripts) and views (under yo/). The controllers and services are in many ways the most significant and often the most complex components (though some views are quite complex as well).
A controller typically manages the relationship between a model and a view. In Topcat it isn't always clear what the model is, though to some extent a lot of the model is contained in the services.
The controller for Topcat's Admin tab. It defines a list of existingTabs, which contains details of the Downloads
and Messages tabs that appear under the Admin tab by default. Further tabs may be added in the configuration, or by
plugins; they are added here via the tc-ui
service's adminTabs() function, using a helpers
function.
Controller for the admin-downloads view, which is used within the Admin tab's Downloads tab. This uses ui-grid to display a table that can show any/all users' downloads. Like many things in Topcat, the grid options are configurable (and in this case at least, there must be some configuration - there is no useful default). The controller always adds a column for the Action buttons (to delete, restore or resume a download, whichever is appropriate). The code is designed to support either paged or scrolled data.
Controller for the admin-messages view, which is used in the Admin tab's Messages tab. This allows admin users to define
a status message (which will be displayed on Topcat's main page after login) or a maintenance mode message (which will
be displayed instead of Topcat's main page); each has a text input box and a toggle box (so for example a service
status message may be defined but not currently shown). The toggle boxes control two Topcat confVar
values.
There is no corresponding view for this controller; instead, it is used within a <ul class="breadcrumb">
element in the browse-entities view. (It is not used in the browse-facilities view, nor anywhere else.)
The controller manages a list of breadcrumb items; the construction of the list is (perhaps) surprisingly complex
and promise-based, because each breadcrumb item text is determined by a query to ICAT.
Controller for Topcat's main browse view, one of the most complex modules in Topcat. I will describe it in a little more detail.
On entry, the entityType is set to the last (dash-separated) word in the current $state name. (The state name is set by tc-icat-entity's browse() function.)
The gridOptions for the list of entities are loaded (ultimately) from the browse configuration for this entity defined in topcat.json.
Several event listeners (using $scope.$on()) are defined, including for a 'global:refresh', in which case the view goes to the first page of results and recalculates everything.
helpers.setupIcatGridOptions() is called, then any custom sort filters or other external filters are applied.
Next, the function generateQueryBuilder() is defined; this has been documented separately. It constructs the ICAT query used to populate the grid view.
The getPage() function calls this then (if required) calculates any size or count columns. Note there is a special case: if there is only a single result from the query and skipSingleEntities is enabled in the configuration, then entity.browse() is called, which will "jump to" the browse view for the entity's children.
updateScroll(): I am not quite sure how this works! Presumably it is somehow triggering the load of more data when the user scrolls past the current "page" of results.
updateTotalItems(): note that this calls generateQueryBuilder().count() (which generates a count() query based on the main browse query).
isAncestorInCart(): uses the $state to determine whether or not the investigation or dataset containing the current entity is already in the user's cart. updateSelections() uses this (and cart membership for each row) to determine whether or not the entity row should be (marked as) selected in the grid view. (In other words, if the parent/ancestor investigation or dataset is in the cart, then all rows will be marked as selected.)
saveState() and restoreState() are used to save and restore the grid state to/from $state.params (as a JSON value). I think this is used to preserve the browse state between user sessions.
showTabs() broadcasts a 'rowClick' event, which loads the metatabs for a row. Originally, it was invoked by clicking anywhere on the row, but this was replaced with a dedicated "info" column and button at the start of the row.
browse(row) is used to drill-down when the user clicks on an active column (e.g. entity name) in the grid view.
The $templateCache tooltip code is something I have never looked at. It seems to be making a distinction based on whether an ancestor is in the cart; perhaps telling the user that they can't remove a row from the cart because the (entire) ancestor is in it?
The selectAll() / unselectAll() functions use isAncestorInCart(); it looks like they do nothing when that returns true. I note in passing that selection / de-selection can require ICAT queries to retrieve details of the entities to be added or removed from the cart. It is possible that the query results have been cached in the client - they should be the same as the original query to populate the browse view.
The next section of code defines a function gridOptions.onRegisterApi, which appears to be setting up the uiGrid behaviour. It calls restoreState(), then getPage() followed by updating the totals and selections. It then defines a number of event callbacks. When sorting or filtering are changed, getPage() etc. are called to refresh the results. rowSelectionChanged() handles the case where a single row is selected or de-selected, and adds or removes the entity from the cart (as ever, only if isAncestorInCart() returns false); rowSelectionChangedBatch() is similar, but for multiple rows. The final section of code in onRegisterApi defines callbacks for the scrolling or paging behaviour.
A separate controller for browsing multiple facilities in Topcat. This is much simpler than the generic entity-browse controller. As far as I know, no production instance uses this, as no production Topcat is configured with multiple facilities.
Controller for the modal dialog to display the user's cart. There are some broad similarities with the browse-entities controller: configurable pagination control, the use of helpers.setupTopcatGridOptions (note, different from setupIcatGridOptions) and finishing with the setup of gridOptions.onRegisterApi, though the details are different.
A list of existingButtons are defined, for Remove All, Download and Cancel; further buttons (if any) are added from tc.ui().cartButtons() (I think this allows plugins and perhaps topcat.json to define extra buttons).
Next, the button methods are defined. cancel() is straightforward.
remove() reconstructs the cart grid data, minus the cartItem to be removed, calls cartItem.delete(), then if the cart is now empty the dialog is dismissed, otherwise the cart cache is cleared and the total size/count is recalculated.
removeAll() creates a list of promises, one per facility (to which the user is currently logged-in), each calling user.deleteAllCartItems(). Once these have completed, the cart dialog is dismissed (since the cart is now empty). I note that there's no obvious error-handling here: should one of the promises fail, it's not clear that the dialog would be dismissed.
download() opens the modal Download Cart dialog, which is described elsewhere.
getCarts() is a promise-based function that returns (a promise to return) the carts for each current facility.
getCartItems() uses getCarts() through a cache, so tries to minimise calls to Topcat' server side.
resetGetTotalsTimeout() is some kind of promisey-womisey timeout thing, which obviously I have skipped!
getDatafilesCount() calculates the total file count for the entire cart (possibly across multiple facilities). It is somewhat complicated because the datafile count for each cart is promise-based, and the inner sum is calculated using the third (function) argument to .then() - I cannot remember offhand why it is done like this!
getTotalSize() is similar.
getTotals() is used when size limits are enabled; it checks whether the count / size limits have been exceeded. (It checks the file count first, and doesn't bother to check the size if the count exceeds its limit). I recall that this was the focus of a recent Topcat issue - it did not work properly. (It was addressed in Topcat 2.4.6.)
(Looking again, I'm not certain what would happen if either the count or size calculation were to fail or time out. It may be that Topcat will effectively assume a cart count / size of zero. However, assuming instead that the limit may have been breached may cause too much inconvenience.)
getTotals() is called immediately in the controller startup.
I think that the definition of gridOptions.onRegisterApi here is similar to that in browse-entities.controller.
This is used for links to Topcat from DOI landing-pages; it opens the browse view for the entity specified in the URL. If the query to retrieve the entity fails (or returns no result), it may be that the data is not yet published, and the user is informed of this via an alert popup.
It has two other functions: to login automatically if so requested in the URL, and to remove itself from the browser history so that the back button behaves as the user expects.
By default, when auto-login is requested, the anonymous authenticator is used. A different authenticator can be specified in topcat.json, but as it requires the authentication password to appear in clear text in topcat.json this is only recommended for development use on a system with restricted access.
The DoiRedirectController is bound to two slightly-different states in app.js. The 'doi-redirect' state is associated
with the URL '/doi-redirect/:facilityName/:entityType/:entityId'
and does not attempt automatic
login; 'doi-redirect-anon' is associated with '/doi-redirect/:facilityName/:entityType/:entityId/:anonLogin'
and will attempt automatic login if the anonLogin
parameter is true
.
The browser history manipulation is performed by passing {location:'replace'}
as an options argument to
entity.browse().
Controller for the Download Cart modal dialog (which is launched from a button on the Cart dialog). It constructs a list of Download objects, one per cart (there is at most one cart for each facility, so in most practical cases there is only a single Download). For each download, the user chooses a transport type (the choice list is derived from the facility's configuration in topcat.json).
isStaged() returns true if any download has a transport type other than http, https or smartclient. This is used in the view to choose which messages to display to the user (e.g. to allow the user to supply an email address to which a message will be sent when the download is ready/staged).
isTwoLevel() returns true if any download uses a transport type that uses a two-level IDS.
In constructing the download objects, the controller uses cart.getSize() to produce an estimated download time. If the getSize() promise fails, then the size is set to -1, which the UI interprets as "unknown".
The ok() button function is complicated by the requirement to check at this point (and not earlier) whether the user has chosen a download transport type that is currently disabled, and to inform them if this is the case. In the normal case, user.submitCart() is called to submit the cart to Topcat's server side. If this succeeds and the download type is http/https and the download status is (already) COMPLETE, then an iframe is added to the current page/document that immediately downloads the data (or at least opens the browser's "Save As..." dialog).
Controller for the modal Downloads dialog that displays a user's downloads. This is simpler than the admin-downloads controller, and does not use the same lazy page loading mechanism.
Controller for Topcat's home page (the one that users see once logged in). Its main job is to construct the list of tabs presented to the user: My Data and Browse are always present, Search can be disabled by configuration, and other tabs can be added via ui.mainTabs().
This controller is bound to the top-level url '/'
in app.js. It sets the new $state depending on the home
property configured in topcat.json: for browse
the state is set to home.browse.facility
;
for any other value x
the state is set to home.x
. I have never looked into how this works in practice.
This is the controller for Topcat's top-level page, index.html (this is not the same as the Home page). I am not certain that I have ever considered index.controller in depth; I have at most considered parts of it in "passing through" when working out parts of Topcat's behaviour. I can only hope that since I have managed to get by without knowing too much about it, so might anyone else!
refreshUserFacilities(): I think this is used on startup, and whenever the user session is changed (e.g. on logout / login). The client-side cache is cleared, and much else is recalculated.
Pages from the configuration in topcat.json (About, Contact, Help, etc.) are added to the left/right sides of the navigation bar if so specified.
There is code here to manage the cart popover ("A new item has been added to the cart"): this.isCartPopoverOpen is set to true whenever 'cart:add' is broadcast; the mechanism by which the flag is set to false (by the function refreshCartItemCount()) is far from obvious! I think that the nested calls to $timeout cause the flag to be cleared a few seconds after the function has been called - but I'm not certain!
There is similar code for the download popover ("A new download has been added"). I have often observed the download popover being stuck "on" , but have never managed to work out why, nor how to fix it!
The code to manage the "download is completed" popover (checkoutForNewlyCompletedDownloads()) is more complex still!
index.controller defines button actions to open the Cart and Downloads dialogs.
refreshSessionInterval stores the ID of a setInterval() function that refreshes the user's ICAT session every 5 minutes (to keep the ICAT session alive so long as the user is logged into the facility in Topcat).
pingSmartclient() is called every minute to "ping" each facility's smartclient. I have no idea what that means, for I have never investigated the use of the smartclient. It appears to be checking the status of all in-progress downloads that use the 'smartclient' transport type.
The final section of code in the controller sets up another interval function that appears to check whether Topcat has been put into maintenanceMode, and if so, logs the user out of all facilities (unless they have admin access).
Controller for the login page (login.html). In app.js, it is the controller for the 'login' and 'login-admin' states.
login(): if the user has chosen an external authentication mechanism, this just broadcasts a 'login:external' event to be picked up elsewhere (though it does not appear anywhere else in Topcat). Otherwise, it passes the chosen authentication method and details supplied by the user to the facility's ICAT. (For a multi-facility Topcat, the user must also choose the facility.) If this succeeds, then if the current state is 'login-admin' (i.e. an attempt to log in as an admin user) then the user is vetted against the adminUsers list. For a non-admin user, if the previous user in this session was different then the session storage is cleared, otherwise it is restored (if not empty).
A relatively-recent modification allows the configuration to assign particular authentication methods to their own buttons, rather than appearing in a choice list. This adds complexity when the UI is mixed, with some but not all authenticators assigned to buttons, especially given that authenticators may or may not require username and password input boxes, and that the login page should be left in a consistent state even if a login attempt fails. buttonLogin(), showCredInputs(), requiresCreds() and facilityChanged() attempt to manage all the permutations that can arise. In practice, the only known site that uses own-button authenticators avoids most of the issues by having only two authenticators. I won't go into details here.
In a related recent modification, the facility configuration may specify extra buttons to appear in the login dialog, where an extra button provides a link to some external URL. (This was motivated by a requirement to provide a link to a registration page for new users.)
This controller is not associated with a view, but is bound in app.js to the states 'logout' and 'logout.facility'. (These states are set by the 'Logout' and 'Logout (all)' buttons on the main page.)
The main job here is to take care if the user logs out while downloads are still at the Preparing stage, or if the smartclient is available. In these cases, the (optional) boolean isSoft argument to icat.login() will be set to true, in which case Topcat will destroy only the browser session, but will not delete the server-side session (and ICAT session).
A small controller for the maintenance-mode view (and state, in app.js). It simply sets the page message if maintenanceMode is on.
Controller for the meta-panel display within the browse views. It defines an event handler for 'rowclick' events, which (despite their name) are now triggered by clicking on the "more info" button in a browse view row. When activated, it loads its configuration from browse.entityType.metaTabs in topcat.json, and uses this to populate a set of tabs that show more detail about the selected row entity. Much of the code here has been described in the documents on Topcat's query construction mechanisms.
There is no corresponding meta-panel.html view. There is a partial-meta-panel.html that displays the metapanel tabs, but it does not refer to the controller; however, the two are linked together in route-creator.service, in a view entry titled '[email protected]'. Similar things appear in app.js. I'm not entirely sure quite why it's done this way!
Controller for the My Data tab. I think that it is similar to, but simpler than, browse-entities. The corresponding view is partial-my-data.html. (I am not sure why browse-entities.html is not called partial-browse-entities.html by analogy.)
Controller for the Search view. The bulk of the code here appears to be fairly straightforward manipulation of the user inputs to the search form (though the Parameter and Sample inputs have their own modal dialogs and controllers).
Interestingly, the search() function does not appear to perform the actual
search; it only constructs the parameters for the search then does $state.go('home.search.results', params)
.
The real work must happen "there"...
(You may surmise from this that I have not inspected the Search code in any great detail - and have never had to!)
Controller for the Seach Parameter modal dialog. This uses a hard-wired query (with no parameters) to obtain the set of ParameterTypes from ICAT, and (I think) uses this to populate the dialog so that users can choose a ParameterType and then supply values depending on those permitted for that type.
Controller for the Search Results view. In app.js, these are bound together under the state 'home.search.results'
with the url:
'^/search?text&startDate&endDate¶meters&samples&facilities&investigation&dataset&datafile'
The controller sets up variables from values in $stateParams that match those set by search.controller's search() function.
The code creates a queryCommon object that I think contains a representation of all the query parameters. I am not sure what stopListeningForCartChanges does, nor why!
The results are displayed in separate tabs, for investigations, datasets and datafiles, depending on which subset of these the user has chosen (or can choose - I believe this can be constrained by configuration).
The real search work is done by tc.search(); most of the code here is about displaying the results.
createGridOptions(type) sets up the gridOptions for each entity type (investigation, dataset, datafile); this includes the definition of a getResults() function that (ultimately) calls tc.search() and then processes the results.
There is a comment in createGridOptions() saying that ideally we should only show an "info" button (to show metadata tabs) when metatabs are defined for that entity in the facility's configuration; unfortunately, this information isn't available to the function at the point where we need to decide. The pragmatic solution is always to add an info button, and accept that sometimes it will do nothing (or say there is nothing to display).
Controller for the Search Samples modal dialog, opened from the Search page. This is very simple, as the dialog input is a single string value.
Controller for the modal Upload dialog, launched from the Browse view (if/when permitted by configuration). The upload mechanism is described in a separate document.
Directives define custom elements and attributes for use in views.
I think this defines a 'compile' attribute that can be set to a string containing HTML markup that is then compiled (wrapped in a span) and added to the document. It is used in several views:
- browse-entities.html : breadcrumb templates
- partial-meta-panel.html : tab items that have templates
Directive and controller for a datetime picker. I have never really looked at this. helpers.setupColumnDef() uses the 'datetime-picker' attribute in the filter header template for columns of type 'date'; this appears to be the only place where it is used. (The date inputs on the Search page do something different.)
I have never looked at this before! I presume it does what it says, and sets things up so that hitting the return key effectively clicks the submit button. At first sight, I thought that it doesn't appear to be used anywhere; but I had forgotten about the sausage-string vs. camel-case convention: an attribute directive named 'onReturnClickSubmitButton' can appear as an attribute named 'on-return-click-submit-button'. As such, it is used in main-search.html, on the text and date inputs.
If I remember correctly, this is a common "trick" in AngularJS stylesheet manipulation. I can't claim to know how it works. It is used in numerous views, often in ui-grid elements.
Directive and controller for the upload-area subcomponent of the Upload dialog. It implements an input into which files can be dragged and dropped. Documented separately under Topcat's upload mechanism.
I think this is the only Element directive used in Topcat. I am not quite certain what it does; perhaps some kind of dynamic insertion of controllers? It is used in browse-entities.html when browseEntitiesController.gridAlternativeView is defined, and sets gridAlternativeController as the controller. Another aspect of Topcat of which I am completely ignorant!
tc-ui.service appears to have a mechanism to register alternative browse grid components (registerBrowseGridAlternative); I presume this can be used by plugins to change the browse view for particular entity types.
Filters define transformations to be applied in-line in AngularJS expressions; see individual filters for examples.
The bytes filter takes a numeric size value (assumed to be in bytes) and expresses it in KB, MB, GB, TB or PB as appropriate (e.g. 2048 | bytes
would appear
as 2KB
).
Actually, the default divisor is 1000, but the filter will use 1024 if the enableKiloBinaryBytes
property is set in topcat.json.
This is used in many places in controllers, services and views (search for |bytes
), typically when displaying entity sizes.
Appears to do what it says, and parses the argument as an integer. However, I don't think it is being used. (There are many uses of parseInt
in Topcat,
but I think these are referring to the same built-in function as is used in this directive itself.)
This appears to convert its argument (which I think is assumed to be in seconds) into a formatted date/time string. I can only find one instance of its use, in download-cart.html, to display an estimated download time based on the connection speed:
{{download.estimatedTime / downloadCartController.connectionSpeed | timeLength}}
Topcat uses AngularJS services in several ways:
- as the interface to an external service, such as ICAT or Topcat's own server side;
- as the interface to more complex parts of the model, such as entities or caches
- to provide general utility functions, in the case of helpers.service
There are quite a few components here at which I have never looked in any detail. Hopefully no-one else will ever have to either!
I have never really looked at this; I had assumed it tied into an external authentication service, but it appears not. The authenticate() function appears to return true so long as there is at least one session in $sessionStorage. I assume that true authentication is managed elsewhere.
This is Topcat's "utility" component, defining numerous functions, some of which are very complex. I will try to cover them all here, though some in more detail than others.
setupColumnDef(columnDef, entityType, translateTitleNameSpace, translateStatusNameSpace)
where columnDef is an initial column definition for the browse view of the entityType, and the translateX arguments are (ultimately, I think) translations for the column name and status. This function is only used in setupTopcatGridOptions() and setupIcatGridOptions(). It adds a number of properties to the columnDef if they are not already defined (i.e., it sets up a number of defaults). The properties include filters, and cell templates that are dependent on the "field" (size, fileSize, datafileCount, datasetCount, status). Note that the code includes a very specific "ISIS hack"!
Takes gridOptions and entityType. Sets up a number of gridOptions properties (some hard-wired, some from configuration); obtains the schema for the entityType
from the topcatSchema (which I think is injected from the topcat-schema.factory), and uses this and setupColumnDef
to (further) set up each columnDef
within the gridOptions.
Used in admin-downloads.controller, cart.controller and downloads.controller, after loading initial gridOptions from the configuration.
Takes gridOptions, entityType and a showInfoButton boolean, and again adds / modifies properties in the gridOptions; however, the code here is considerably
more complex. The icatSchema (probably injected from icat-schema.factory) is used to map variables in columnDef fields to variables in the schema.
(Yes, I'm being vague here because I've not looked at this in detail. This looks similar to how the schema is used in query construction.)
There is code to handle columns that have the excludeFuture property set (adding a filter based on the current date).
A (surprisingly complicated) default cellTemplate is defined if none has been specified in the configuration.
If the showButton argument is true, an extra column with an "info" icon is prepended to the list of columns.
Finally, a column is added to the end containing any "action buttons", such as a Download button, or buttons defined via tc.ui().entityActionButtons()
.
Used in the controllers browse-entities, browse-facilities, my-data and search-results, after loading initial gridOptions from the configuration.
Takes a list of sortColumns. It appears to return a function that compares two entities according to the hierarchy of sort orders defined in the column definitions (the function returns -1, 0 or 1). It looks fiddly but I'm fairly sure it's doing the right thing!
Used by cart.controller, downloads.controller and search-results.controller, in each case in the gridApi code to react to changes in the sort ordering.
Takes a gridOptions object and returns a function that takes a row (entity) and returns true if the entity passes all the filters defined on the columns.
Used by cart.controller, downloads.controller and search-results.controller, in each case in the gridApi code to react to changes in the filtering.
Takes arrays of named existingObjects and toBeMergedObjects (i.e. maps from names to objects) and, well, merges them.
The finer detail is that each toBeMergedObject can optionally specify that it should be inserted before or after another named object.
Used in admin.controller and home.controller to merge together default and custom-configured tabs; and in cart.controller to do the same for buttons. Also used in helpers.setupIcatGridOptions for the action buttons.
These "fill in the blanks" on a partially specified date, depending on whether it is a from- or to-date.
Used in the admin-downloads, browse-entities and my-data controllers, all in query construction; and in helpers.generateEntityFilter.
Extends the builtin typeof operator; takes an object and returns a string representing its type. The differences from the builtin are that it will return
'null'
for null values, 'array'
for arrays and 'promise'
for functions.
Most of the uses are elsewhere in the helpers service (in overload() and buildQuery()), but it is also used in tc-icat.service's query() method.
This is used to implement a form of overloaded function definitions. It is widely used in other Topcat services (it doesn't appear to be used in any other components), and takes some getting used to! At heart, it takes a list of variations, where each variation contains a list of type-strings and a function. It returns a function that, given a list of arguments, looks for a matching variation (using helpers.typeOf()) and (if found) applies the corresponding function to the arguments.
In general use one variation will be the "base case", with other variations implemented in terms of it. Here's an example from tc-icat-entity.service (with extra comments):
this.getSize = helpers.overload({
/**
* Returns the total size of an investigation or dataset. Only applies to investigations and datasets.
*
* @method
* @name IcatEntity#getSize
* @param {object} options {@link https://docs.angularjs.org/api/ng/service/$http#usage|as specified in the Angular documentation}
* @return {Promise<number>} the deferred total size of this investigation or dataset
*/
'object': function(options){
// This is the "base" case - all other variations are defined in terms of this
var that = this;
this.isGettingSize = true;
return icat.getSize(this.entityType, this.id, options).then(function(size){
/**
* total size of an investigation or dataset. Only applies to investigations and datasets, and only gets set after calling <code>getSize()</code>.
*
* @name IcatEntity#size
* @type {number}
*/
that.size = size;
that.isGettingSize = false;
return size;
}, function(response){
// error handler - getSize request failed; use -1 as "unknown size"
var msg = response?' entity getSize failed: ' + response.code + ", " + response.message : ' response is null';
console.log(that.entityType + msg);
that.size = -1;
that.isGettingSize = false;
return -1;
});
},
/**
* Returns the total size of an investigation or dataset. Only applies to investigations and datasets.
*
* @method
* @name IcatEntity#getSize
* @param {Promise} timeout if resolved will cancel the request
* @return {Promise<number>} the deferred total size of this investigation or dataset
*/
'promise': function(timeout){
// Calls the base case with the timeout added to the options
return this.getSize({timeout: timeout});
},
/**
* Returns the total size of an investigation or dataset. Only applies to investigations and datasets.
*
* @method
* @name IcatEntity#getSize
* @return {Promise<number>} the deferred total size of this investigation or dataset
*/
'': function(){
// No-arguments case: calls the base case with empty options
return this.getSize({});
}
});
Note how each variation has been given its own docstring.
Re-quotes quote characters in strings (that are about to be incorporated into JPQL). It's worth going into a little more detail here: the actual code is:
this.jpqlSanitize = function(data){
if(typeof data == 'string' && !data.isSafe){
return "'" + data.replace(/'/g, "''") + "'";
}
return data;
};
The test !data.isSafe
needs some explanation: later in helpers.service, Topcat defines a SafeString
object:
String.prototype.safe = function(){
return new SafeString(this);
};
function SafeString(value){
this.isSafe = true;
this.value = value;
}
SafeString.prototype.toString = function(){
return this.value;
};
This enables callers to "declare" that a string is safe by adding ".safe()" to the expression, e.g. columnDef.field.safe()
; then, if
helpers.jpqlSanitize() is called on the value, it will not do any quote conversion.
In effect, .safe()
is a declaration, not a conversion to a "JPQL-safe" string.
This performs late-stage processing on a query (the argument can be a nested structure containing lists of strings and functions). It has been documented separately as part of Topcat's query-building mechanism.
Takes a list of key/value pairs and generates a URL argument-sequence from them (key1=value1&key2=value2&...
, where the keys and values have been url-encoded).
Hopefully obvious!
I think this converts from sausage-case
to SAUSAGE_CASE
, and from CamelCase
to CAMEL_CASE
.
It appears to be used elsewhere to convert values to (parts of) translation string keys (as used in lang.json).
The next section of helpers.service sets up data structures and timer functions for processing, monitoring and reporting on the behaviour of Topcat's low-priority queue. Originally, the code here just processed the queue; the ability to monitor and report on the queue performance was added later.
In generateRestMethods (see later), the caller can specify a lowPriority option, in which case the method request is added to the lowPriority queue, and only evaluated when it pops to the top of the queue. This was done to prevent possibly-expensive requests throttling other behaviour.
lowPriority is set in tc-icat-entity.service (getDatasetCount, getDatafileCount), tc-icat.service (getSize) and index.controller (pingSmartClient).
This is a particularly convoluted function! It adds get, delete, post and put functions to the caller that when called will generate an HTTP GET/DELETE/POST/PUT request based on the arguments (or will add the request to the low-priority queue). The functions are defined through helpers.overload() so can be called in multiple forms.
An example might help. The Icat(facility) function (effectively an object constructor) in tc-icat.service uses generateRestMethods like this:
helpers.generateRestMethods(this, facility.config().icatUrl + '/icat/');
so the function's arguments are the Icat object itself, and a prefix string of the form 'https://some.icat.server/icat/'.
The main body of generateRestMethods
does:
defineMethods.call(that, 'get');
defineMethods.call(that, 'delete');
defineMethods.call(that, 'post');
defineMethods.call(that, 'put');
This causes the methods get(), delete(), post() and put() to be added to the Icat service object. Each function takes an offset, and possibly a params string or data
object and an options object. For example, icat.get('version') will (normally) send GET https://some.icat.server/icat/version
.
It also defines methods getUrlLength(), deleteUrlLength(), etc. (The details of this are omitted in what follows.)
The definition of defineMethods
is complex; there are three levels of nested function definitions here!
function defineMethods(methodName){
this[methodName] = helpers.overload({
'string, string, object': function(offset, params, options){
return send(offset, params, options);
},
'string, array, object': function(offset, data, options){
return send(offset, data, options);
},
'string, object, object': function(offset, params, options){
return this[methodName].call(this, offset, helpers.urlEncode(params), options)
},
// ...other variants defined in terms of the first two...
so for each of the four REST methods, this defines overloaded versions that all (ultimately) call another function, send()
, which takes three arguments:
a string, a string or array, and an 'options' object.
send()
is defined within defineMethods
, and is also not straightforward: it begins by (possibly) setting some method headers, and constructing
a URL:
function send(offset, data, options){
options = _.clone(options);
if(methodName.match(/post|put/)){
if(!options.headers) options.headers = {};
if(!options.headers['Content-Type']) options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
var url = prefix + offset;
if(methodName.match(/get|delete/)){
if(data !== '') url += '?' + data;
} else if(options.queryParams) {
url += '?' + helpers.urlEncode(options.queryParams);
}
so for example, get(offset,params,options)
the URL will (in this case) be of the form:
https://some.icat.server/icat/<offset>?<params>
However, the work of generating the REST request is carried out by yet more inner functions:
var out = $q.defer();
function call(){
if(options.lowPriority) lowPriorityCounter++;
if(options.bypassInterceptors){
var xhr = $.ajax(url, {
method: methodName.toUpperCase(),
headers: options.headers,
data: methodName.match(/post|put/) ? data : undefined
});
xhr.then(function(data){
success({data: data})
}, function(qXHR, textStatus, errorThrown){
failure({data: errorThrown})
});
if(options.timeout){
options.timeout.then(function(){
xhr.abort();
});
}
} else {
var args = [url];
if(methodName.match(/post|put/)) args.push(data);
args.push(options);
$http[methodName].apply($http, args).then(success, failure);
}
}
so call()
either does some fancy XHR stuff, or uses the $http
service to execute the request directly. Note the optional use of a lowPriorityCounter (for the lowPriorityQueue set up earlier in helpers.service); and note that this is where post/put data is added to the request.
In either case, success or failure are handled by separate functions that either resolve or reject the promise with the response data:
function success(response){
out.resolve(response.data);
if(options.lowPriority) lowPriorityCounter--;
}
function failure(response){
// BR: have observed "TypeError: response is null" errors;
// probably not coming from here, but let's log anyway
if(response == null){
console.log("Failure response from " + methodName + " " + prefix + offset + " is null");
} else if(response.data == null){
console.log("Failure response from " + methodName + " " + prefix + offset + " has no/null data");
}
out.reject(response?response.data:null);
if(options.lowPriority) lowPriorityCounter--;
}
The final part of defineMethods()
either calls call()
directly, or pushes it onto the low priority stack, before returning the promise that will
eventually be resolved or rejected when the request completes (or times out, I suppose):
if(options.lowPriority){
if( monitorLowPriorityQueue ) lowPriorityTimes.push((new Date).getTime());
lowPriorityQueue.push(call);
} else {
call();
}
return out.promise;
I hope that's clear.
I don't think I've ever considered this function.
throttle(size, delay, timeout, items, fn)
appears to split the list of items into chunks specified by the size, and apply the function to each item in a chunk, with a delay between each chunk. It is used in several places in tc-user-cart.service's getSize(), getDatafileCount() and in an initial block to calculate the size of each cart item.
Near the end of helpers.service is a curious-looking section of code:
/**
* A defered asynchronous call
*
* @interface Promise
*/
(function(){
var methods = {
get: $http.get,
delete: $http.delete,
post: $http.post,
put: $http.put
};
_.each(methods, function(method, name){
$http[name] = function(){
return extendPromise(method.apply(this, arguments));
};
});
...
})();
This block defines an extendPromise(promise) function that appears to add a log() function to its promise argument; it is applied to the $http methods (as above) and to the promise methods $q.all, $q.defer, $q.resolve and $q.reject.
Described under jpqlSanitize above.
Another component I've never looked at! It appears to add an 'http:error'
broadcast on HTTP errors.
The only other reference to responseError
in the code is something similar (but simpler) in app.js.
A model of the ICAT schema, including relationships between entities. There is code at the end to add "variable paths" to each entity, and to add a mapping from relationship variable names to entity types. This has been described in more detail in the documents on Topcat's query-building mechanism.
This defines the schema for topcat.json. Any new configuration properties need to be defined here. topcat.json is checked against this when Topcat starts, and any validation failures are shown to the user as alert boxes.
I've never looked at this before! It appears to add states to map page-name urls (e.g. "help") to the page content under content/pages/. It is called near the end of app.js (so near the end of Topcat's startup, I think).
Another service I have never investigated. On a brief skim, it appears to derive routes from the facility hierarchy; perhaps for drilldown, history or breadcrumbs? Of all the functions defined here, only getAllRoutes appears to be used elsewhere, in route-creator.service.
Appears to define the routes for home.browse.facility.*
, and ties them to the browse-entities and meta-panel views / controllers.
Used in app.js at the same point as the page-creator service.
Yet again, I've never looked at this before. It appears to allow support for injection of configuration-time and run-time dependencies. Possibly used by plugins?
The main entry point for Javascript API. This has reasonable docstrings so I won't go into more detail here.
This implements the interface to Topcat's server-side /admin/ API. Again, the methods have reasonable docstrings.
The downloads() function uses the admin API to receive a list of downloads; note that it then adds delete() and restore() methods to each download.
Implements the Cache class that is used for several client-side caches. Caches are created by tc-icat.service (queries, entity sizes), tc-ids.service (not used?) and tc.service (lucene search results). As far as I can tell, the cache created by tc-ids.service is never used (but I may be missing something).
The class constructor takes an optional dontCache
function that takes a key and value; if it returns true, then the value will not be stored in the cache.
This is used when Topcat is configured not to cache zero-sized investigations, etc; an example from tc-icat.service is:
// Set up a dontCache filter function for zero-sized investigations, if required
var dontCache = null
if(tc.config().dontCacheZeroSizedInvestigations){
dontCache = function(key,value){
return ( value == 0 && key.startsWith("getSize:investigation") );
}
}
Each cache has a monitoring capability that is designed to be turned on (for all caches) from the browser console by executing `tc.setMonitoring(true)'. When monitoring is enabled, caches record and report various statistics including hits, misses and "wasted promises". (Wasted promises occur when multiple promises to calculate the same key are started with each 'believing' that it should run to completion and then update the cache. It is difficult to see how this can be avoided.)
The method get(key, seconds, fn)
is not a straight cache-retrieval function: if the key is not found in the cache, or if seconds is greater than zero
but less than the age of the stored value, then the function is invoked to retrieve a value, which is then stored in the cache. This means that individual
get() requests can specify an age limit (though in practice it is set by configuration).
The method getPromise(key, seconds, fn)
is similar, but fn
is run as a promise, and the cache is not updated until it completes (hence the
possibility of "wasted promises" above).
This manages access to the properties of a Facility. It is fairly straightforward.
This provides an interface for the ICAT for a Facility, in part through Topcat's server-side /icat/ API. I will say a little more about some of the larger functions here.
There are two overloaded versions: one takes an authentication plugin name and a credentials map, the other takes a plugin name, username and password (and calls the first version using the obvious credentials construction). First, the plugin and credentials are POSTed to this facility's ICAT's /session/ API. If that succeeds, then it sets up the username and plugin in the session storage for the facility. Next, it constructs a list of promises:
- query ICAT for the facility ID
- if an idsUploadDatasetType (name) is configured, query ICAT for its ID
- ditto for idsUploadDatafileFormat
- ask the Admin interface whether this user has (Topcat) Admin rights
- query ICAT for the user's full name (defaulting to the username if it is not defined)
- use the server-side /user/downloads API to build a list of keys for the user's downloads. (The list is called
completedDownloads
but I think it will contain all undeleted downloads.)
Once (if) all these promises have completed, login() broadcasts several events (session:change
,session:changed
and session:add
) that
trigger other actions elsewhere. session:change
triggers refreshUserFacilities()
in index.controller (a general recalculation).
session:changed
"passes" the completedDownloads
list to index.controller (and clears the isSessionChanging
flag).
session:add
doesn't appear to be used anywhere else (perhaps it may be used by plugins?)
Should the initial session request fail, the reason is logged to the console and (I think!) reported to the user.
Destroys the user's session (for this facility). The base case of this function takes a boolean argument, isSoft; as the docstring says, if this is true the session will only be destroyed in the browser but not on the server.
If I remember correctly, isSoft is set to true if the user has downloads that are still in progress; that way, the server session is kept alive at least until the downloads have completed.
This is one of Topcat's main query-construction methods; see separate documentation on Topcat's query constructions.
This service represents an ICAT entity (in practice, an investigation, dataset or datafile, plus 'proposal', which is not a true ICAT entity but represents a hierarchy layer between facility and investigation in some facilities).
Instances are created by:
- itself: to 'complete' setup of investigations and datasets; to create child entities
- tc-icat-query-builder.service: used to create proposal entities. Attributes: proposal (entityType, name, id, investigations)
- tc-icat.service: used by query() to create entities from the results. Attributes: query result + entityType
- tc-user-cart.service: used by getDatafileCount() to create investigation or dataset entities. Attributes: entityType, id
- tc.service: used by search() to create entities from the query results. Attributes: query result + facilityName, score (in lucene).
Instances are created with a map of attributes and a facility. The attributes become properties of the new entity. Functions are defined on the entity depending on its entityType; for example, getDatasetCount() is only defined for investigations.
A large part of the code defines material used in construction of the state parameters for the entity (along with those for its ancestors in the hierarchy): parentQueries, isValidPath, findPath, thisAndAncestors, stateParams. This adds the ID for the entity (using the Investigation name for a proposal) and each ancestor to the state parameters; so for example, the state params for a Dataset would include investigationId, proposalId (the Investigation name) and datasetId.
If the original attributes supplied to the constructor include a dataset or investigation object, then these are replaced by new entity constructions.
I have never looked (nor had to look) at the code for the find(expression) function.
Near the end there is code that I think replaces any (references to) child entities within the entity with new entity constructions; again, this is code I have never considered.
The final fragment of code allows plugins to define functions that extend the behaviour of specific entity types. I don't know whether this has been used in earnest.
A central component of Topcat's query-building mechanism. This has been documented elsewhere.
This service manages the interface to Topcat's server-side /ids/ API (which then communicates with an IDS). The main function here is upload(), which has been documented separately.
I have never really looked at this. It appears to implement a connection to an IDS for the 'smartclient' download transport type, with requests going to a local service on port 8888.
This service manages access to parts of the UI that can be modified by plugins: the main tabs, admin tabs, cart buttons, entity action buttons and (auxillary) pages (help etc.) For each of these it provides methods that can be used by plugins to register new tabs, buttons or pages (registerMainTab etc.). There are also functions to register external grid filters and alternative browse grids for specific entity types.
This service manages the interface to Topcat's server-side /user/ API (implemented there by the UserResource class). There are two main groups of functions here: one set to manipulate the user's downloads, and one to manipulate the user's cart.
The function downloads() retrieves the user's downloads from Topcat's server side. There are overloaded versions, but most take a queryOffset argument, which can be a string or an array; the array can contain a parameterised query and its parameter values. Note that the function adds a delete() function to each download.
The function cart() retrieves the contents of the user's cart from the server side (it is kept there so that it can be preserved between sessions). (To be more precise, the cart is cached locally, and so only retrieved on the first request.) It uses tc-user-cart.service to create a local Cart object.
addCartItems() takes care only to add items that are not already in the cart. If anything is to be added, this is done through a POST to the server-side's /cart/facility/cartItems endpoint; this returns the new cart, which then replaces the locally-cached cart.
deleteCartItems() is quite different: since the REST DELETE operation's parameters are supplied in the URL, it has to use chunking to avoid creating overlong URLs. (This is not necessary for POST operations.)
submitCart() sends a POST request to the server side to create a new download for the cart contents. The request returns a new cart, which will be empty.
This service manages the user's cart. I notice that the code refers to this.cartItems, but never defines it. I believe that cartItems are created by the server-side; but note that code (near the end) here defines new attributes (including functions) for each cartItem.
getSize() determines the total size of the cart by summing requests to ICAT for the size of each entity in the cart. Note that if the request fails for an entity then the total size is set to -1; this is interpreted elsewhere as 'unknown', but I am not sure how this affects any checks on size limits. There is no corresponding failure-handling in getDatafileCount(). Both methods use helpers.throttle() to attempt to ensure that a flurry of requests does not grind the UI to a halt.
The functions added to each cartItem are:
- delete() - simply calls back to user.deleteCartItem()
- entity() - sends a query to ICAT to retrieve the corresponding entity
- getSize() - for a datafile, returns its size; for other entities, calls entity.getSize()
- getDatafileCount() - for a datafile, returns 1; for other entities, calls entity.getDatafileCount()
The penultimate section of code in the service is:
$timeout(function(){
var timeout = $q.defer();
var stopListeningForCartOpen = $rootScope.$on('cart:open', function(){
stopListeningForCartOpen();
stopListeningForCartChange();
timeout.resolve();
});
var stopListeningForCartChange = $rootScope.$on('cart:change', function(){
stopListeningForCartOpen();
stopListeningForCartChange();
timeout.resolve();
});
helpers.throttle(10, 10, timeout.promise, that.cartItems, function(cartItem){
return cartItem.getSize({
timeout: timeout.promise,
bypassInterceptors: true
});
});
});
I am not certain what most of this does! The last part appears to be triggering an initial calculation of the size of each cartItem.
This defines the schema for the entities used in Topcat's server side's persistent storage: cart, cartItem, download, downloadItem and parentEntity. This appears only to be referenced directly by helpers.setupTopcatGridOptions, where it is used to determine the type of each column's field.
Perhaps I should have covered these first! But these are parts of Topcat that I've not seemed to need to know in any depth, even though they're what brings it all together; and I don't think they make much sense without understanding something about the components that they use.
This defines 'app' as the top-level Topcat module; but it does much more besides. I can't claim to understand everything that's in here...
This takes a function fn
and applies it to the last (?) script in the document, then stores the result in a plugins
list.
It doesn't appear to be called from anywhere within the Topcat code, so I presume it's used by plugins to register themselves with Topcat.
A large section of code defines a "deferred bootstrapper" - whatever that is! I presume that the resolve
element is something to do with property injection.
APP_CONFIG
is mapped to a function that:
- determines which configuration file to load (topcat_dev.json on specific (development) ports, topcat.json otherwise).
- parses the JSON in topcat.json (this is where the infamous "Invalid topcat.json" alert comes from).
- for each configured facility, if icatUrl is not defined then this queries the idsUrl for it. (One consequence of this is that if ANY facility is configured this way, then if that facility does not respond, or cannot be reached, then Topcat can fail during startup - even if no-one tries to log into that facility. This has caused problems with development instances that have ISIS dev in their configuration (including the original Travis build!) I modified the code here so that it will attempt to continue without the facility; but the alert still caused issues with Travis. This is why the topcat.json used for Travis contains only the LILS facility.)
- if a facility's configuration does not specify any authenticators, it queries the facility's ICAT for its list of authenticators. So topcat.json need not list the authenticators (unless it wants to change any properties), so long as ICAT can be queried for them.
- determines whether or not
maintenanceMode
is on. - finally, a large chunk of code adds any plugin scripts and stylesheets to the document, then waits for them to load before continuing.
LANG
is mapped to a function that attempts to parse lang.json.
At this point, the Topcat main module app
is created.
Next is another chunk of code (an IIFE) that has something to do with checking whether or not all plugins have loaded. I last looked at this a long time ago; if I remember correctly it works by adding an extra function call to be called last by each plugin that sets a flag that tells Topcat that the plugin has finished loading.
The next code fragment:
app.run(function(APP_CONFIG, LANG, objectValidator){...
appears to validate Topcat's configuration against any configuration schemas defined by plugins. I think this also validates against Topcat's own schema.
I think these are fairly trivial / verge on boilerplating. I've always ignored them!
This defines the top-level state mapping that maps states (such as 'home', 'home.browse', 'login') to urls, templates/views and controllers. I think it's fairly self-descriptive for the most part. A few points:
- Note how maintenance mode is handled; I think this defines 'maintenance-mode' as the only possible state when maintenanceMode is on.
- the url for 'home.search.results' contains what looks like declarations of its parameters
- a few urls (e.g. for 'doi-redirect') form REST-like paths ('/doi-redirect/:facilityName/:entityType/:entityId')
This appears to add request counting to HTTP methods (and failures).
- something about cross-site scripting (comment: "add plugin hosts to cross site scripting white list")
- something about setting $rootScope.$state and $rootScope.$stateParams
- a declaration of event handlers for:
- $stateChangeStart - save the last state (unless the user is logging in or out?)
- $stateChangeError - on authentication errors go to the 'login' state. Note the use of location: 'replace' here to preserve browser Back-button behaviour
- something about the PageCreatorService and RouteCreatorService
- something about running plugin setups (with timeouts?)
I have never looked at this before, have no idea why it's here, or even if it is still being used.
Another file I have never looked at before. It claims to be a parser generated by "Jison".
For the most part the views are fairly obvious, with the real work being done in the corresponding controllers.
Topcat's main page. Pulls in all the scripts. Defines the navigation bar, footer and cookie message.
I recall adding the 'loading_message' div to display "Loading..." during startup; but I can't remember how it works with the ng-cloak attribute on the body! The intention was to avoid showing just a blank screen while Topcat starts up; and if Topcat hangs (or dies) during startup, then "Loading..." is left on-screen, which was considered (slightly) better than a mere blank page!
Uses home.controller to iterate over the main tabs. The active tab is determined by $state.includes(tab.showState)
.
The Admin tab view. Uses admin.controller to iterate over the admin tabs.
View for the Downloads tab within the Admin view. If there are multiple facilities then a separate tab (and downloads list) is shown for each.
The downloads view itself is typical for ui-grid views:
<div ng-if="adminDownloadsController.gridOptions" class="col-md-12">
<div ui-grid="adminDownloadsController.gridOptions" ui-grid-infinite-scroll class="browse-grid" take-up-remaining-height ui-grid-resize-columns>
<div class="no-rows row" ng-show="isEmpty">
<div class="col-md-2 col-md-offset-5 empty-message">
<span translate="BROWSE.EMPTY"></span>
</div>
</div>
</div>
</div>
The real work is done by the ui-grid configuration in the admin-downloads.controller; here the content of the ui-grid div defines the content when the grid is empty.
The Admin Messages view. This is defined as a form with fields for the Service Status and Maintenance Mode messages and toggles.
This defines the contents of the Browse view. The top row contains the breadcrumbs (note the use of compile
so that markup in the breadcrumb template is evaluated) and the total count. Within the count div,
<span ng-if="browseEntitiesController.totalItems === undefined" class="loading"> </span>
triggers the (css-defined) 'spinner' to appear when totalItems
has not (yet) been calculated.
If uploading is enabled, an Upload button is added.
There are placeholders for an alternative grid view (possibly defined by plugins). Otherwise, the normal grid view is shown. There are two near-identical divs, for scrolled and paginated views. Both contain divs that are shown when there are no results, or when the content is loading.
The top-level browse view when Topcat is configured for multiple facilities. This is similar to, but much simpler than, the generic browse-entities template.
Content of the modal dialog for the user's cart. There are alternative ui-grid divs for scrolled and paginated views. The footer shows message(s) if size/count limits are exceeded, and an iterated button element for each of the controller's defined buttons.
View for the datetime-picker directive. I've never really looked at this. It uses uib-datepicker to provide a calendar/time input dialog on date-type columns in the browse views.
View for the Download Cart dialog. For once, this uses a table rather than a ui-grid for its contents. The column headers are: (target) file name, download transport type, size, estimated download time (with a connection speed choice list) and facility name (when there are multiple facilities). The rows iterate over the download-cart.controller's downloads list. (I believe that there can only be one entry per facility.) There is a text input field for the filename (with a generated default) and a choice list for the download types. Below the table are placeholders for is-archived / is-staged messages, and an input box for an email address for two-level / staged downloads. The table is replaced by a "Submitting..." message during submission. (This can get stuck if submission fails.) The footer contains Submit and Cancel buttons.
View for the user's (modal) Downloads dialog; essentially a ui-grid view (with scrolled/paged alternatives).
The login view. This is fairly straightforward. The choice inputs for Facility and Authentication Type are only present when there are choices.
The username / password inputs are only shown when loginController.showCredInputs()
returns true. Originally this was the case when the chosen authenticator required them; but now they need to appear if any button-based authenticator requires them.
This causes some complexities with form validation if a button authenticator requires inputs but the user chooses a choice-list authenticator that does not require them. Ideally, configurations that allow this should be avoided!
There is a Login button to use the authenticator chosen from the choice list (if any), and buttons for each authenticator configured with its own button; followed by any extra buttons defined in the configuration.
A tiny view that acts as a template for the home.browse
state, with placeholders for the browse and metatabs content.
A tiny view that acts as a template for the home.my-data
state, with placeholders for the browse and metatabs content.
View for the Search tab; contains more detail than the other main- views. There is a sidebar for the numerous search inputs, and placeholders for the results and metatabs content.
Page shown when maintenanceMode is on.
Contains the tabs for the metatabs panel - the "info" view that shows details for a selected entity row in browse views.
The contents of the My Data tab.
When there are multiple facilities, it shows a separate tab for each one.
There's a placeholder for (templates defined by) any externalGridFilters.
Similar to other browse views, there are two ui-grid divs, for scrolled and paginated views, each with "no content" and "loading" divs.
I can find no references to this anywhere else. It looks like it may have been for a dialog to confirm removal of a download; but I don't know if it was ever used.
View for the search-parameter.controller. Used to add a new parameter filter. There is an initial choice list of parameter types, and a large div that provides different inputs based on the type selection:
- for strings, free input or choice from a configured list, depending on the parameter configuration
- for numeric and date types, a choice for Match or Range filters
- numeric match or range inputs
- date match or range inputs
View to display search results. This shows up to three headers (for investigations, datasets and datafiles) depending which are enabled in the configuration. For each tab there is a corresponding grid view, with divs for "loading" and "no results". Unlike the other browse grid views, only scrolling is supported - there is no separate pagination case.
View for the modal dialog to input a Sample search string.
Almost a one-liner! Shown on first use of the Search tab, before there are any results to show. In app.js, associated with the state 'home.search.start' and the URL '/start'.
Used in the Upload mechanism, described elsewhere.