mirror of
https://github.com/nasa/openmct.git
synced 2024-12-30 18:06:59 +00:00
[Features] Added Autoflow Tabular to open source features. Fixes #1469
This commit is contained in:
parent
12d1302138
commit
4ae35576a5
51
platform/features/autoflow/plugin.js
Executable file
51
platform/features/autoflow/plugin.js
Executable file
@ -0,0 +1,51 @@
|
|||||||
|
define([
|
||||||
|
'text!./res/templates/autoflow-tabular.html',
|
||||||
|
'./src/AutoflowTabularController',
|
||||||
|
'./src/MCTAutoflowTable'
|
||||||
|
], function (
|
||||||
|
autoflowTabularTemplate,
|
||||||
|
AutoflowTabularController,
|
||||||
|
MCTAutoflowTable
|
||||||
|
) {
|
||||||
|
return function (openmct) {
|
||||||
|
openmct.legacyRegistry.register("platform/features/autoflow", {
|
||||||
|
"name": "WARP Telemetry Adapter",
|
||||||
|
"description": "Retrieves telemetry from the WARP Server and provides related types and views.",
|
||||||
|
"resources": "res",
|
||||||
|
"extensions": {
|
||||||
|
"views": [
|
||||||
|
{
|
||||||
|
"key": "autoflow",
|
||||||
|
"name": "Autoflow Tabular",
|
||||||
|
"cssClass": "icon-packet",
|
||||||
|
"description": "A tabular view of packet contents.",
|
||||||
|
"template": autoflowTabularTemplate,
|
||||||
|
"needs": [
|
||||||
|
"telemetry"
|
||||||
|
],
|
||||||
|
"delegation": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"controllers": [
|
||||||
|
{
|
||||||
|
"key": "AutoflowTabularController",
|
||||||
|
"implementation": AutoflowTabularController,
|
||||||
|
"depends": [
|
||||||
|
"$scope",
|
||||||
|
"$timeout",
|
||||||
|
"telemetrySubscriber"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"directives": [
|
||||||
|
{
|
||||||
|
"key": "mctAutoflowTable",
|
||||||
|
"implementation": MCTAutoflowTable
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
openmct.legacyRegistry.enable("platform/features/autoflow");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
26
platform/features/autoflow/res/templates/autoflow-tabular.html
Executable file
26
platform/features/autoflow/res/templates/autoflow-tabular.html
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
<div class="items-holder abs contents autoflow obj-value-format"
|
||||||
|
ng-controller="AutoflowTabularController as autoflow">
|
||||||
|
<div class="abs l-flex-row holder t-autoflow-header l-autoflow-header">
|
||||||
|
<mct-include key="'input-filter'"
|
||||||
|
ng-model="autoflow.filter"
|
||||||
|
class="flex-elem">
|
||||||
|
</mct-include>
|
||||||
|
<div class="flex-elem grows t-last-update" title="Last Update">{{autoflow.updated()}}</div>
|
||||||
|
<a title="Change column width"
|
||||||
|
class="s-button flex-elem icon-arrows-right-left change-column-width"
|
||||||
|
ng-click="autoflow.increaseColumnWidth()"></a>
|
||||||
|
</div>
|
||||||
|
<div class="abs t-autoflow-items l-autoflow-items"
|
||||||
|
mct-resize="autoflow.setBounds(bounds)"
|
||||||
|
mct-resize-interval="50">
|
||||||
|
<mct-autoflow-table values="autoflow.rangeValues()"
|
||||||
|
objects="autoflow.getTelemetryObjects()"
|
||||||
|
rows="autoflow.getRows()"
|
||||||
|
classes="autoflow.classes()"
|
||||||
|
updated="autoflow.updated()"
|
||||||
|
column-width="autoflow.columnWidth()"
|
||||||
|
counter="autoflow.counter()"
|
||||||
|
>
|
||||||
|
</mct-autoflow-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
169
platform/features/autoflow/src/AutoflowTableLinker.js
Executable file
169
platform/features/autoflow/src/AutoflowTableLinker.js
Executable file
@ -0,0 +1,169 @@
|
|||||||
|
/*global angular*/
|
||||||
|
define(
|
||||||
|
[],
|
||||||
|
function () {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The link step for the `mct-autoflow-table` directive;
|
||||||
|
* watches scope and updates the DOM appropriately.
|
||||||
|
* See documentation in `MCTAutoflowTable.js` for the rationale
|
||||||
|
* for including this directive, as well as for an explanation
|
||||||
|
* of which values are placed in scope.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param {Scope} scope the scope for this usage of the directive
|
||||||
|
* @param element the jqLite-wrapped element which used this directive
|
||||||
|
*/
|
||||||
|
function AutoflowTableLinker(scope, element) {
|
||||||
|
var objects, // Domain objects at last structure refresh
|
||||||
|
rows, // Number of rows from last structure refresh
|
||||||
|
priorClasses = {},
|
||||||
|
valueSpans = {}; // Span elements to put data values in
|
||||||
|
|
||||||
|
// Create a new name-value pair in the specified column
|
||||||
|
function createListItem(domainObject, ul) {
|
||||||
|
// Create a new li, and spans to go in it.
|
||||||
|
var li = angular.element('<li>'),
|
||||||
|
titleSpan = angular.element('<span>'),
|
||||||
|
valueSpan = angular.element('<span>');
|
||||||
|
|
||||||
|
// Place spans in the li, and li into the column.
|
||||||
|
// valueSpan must precede titleSpan in the DOM due to new CSS float approach
|
||||||
|
li.append(valueSpan).append(titleSpan);
|
||||||
|
ul.append(li);
|
||||||
|
|
||||||
|
// Style appropriately
|
||||||
|
li.addClass('l-autoflow-row');
|
||||||
|
titleSpan.addClass('l-autoflow-item l');
|
||||||
|
valueSpan.addClass('l-autoflow-item r l-obj-val-format');
|
||||||
|
|
||||||
|
// Set text/tooltip for the name-value row
|
||||||
|
titleSpan.text(domainObject.getModel().name);
|
||||||
|
titleSpan.attr("title", domainObject.getModel().name);
|
||||||
|
|
||||||
|
// Keep a reference to the span which will hold the
|
||||||
|
// data value, to populate in the next refreshValues call
|
||||||
|
valueSpans[domainObject.getId()] = valueSpan;
|
||||||
|
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new column of name-value pairs in this table.
|
||||||
|
function createColumn(el) {
|
||||||
|
// Create a ul
|
||||||
|
var ul = angular.element('<ul>');
|
||||||
|
|
||||||
|
// Add it into the mct-autoflow-table
|
||||||
|
el.append(ul);
|
||||||
|
|
||||||
|
// Style appropriately
|
||||||
|
ul.addClass('l-autoflow-col');
|
||||||
|
|
||||||
|
// Get the current col width and apply at time of column creation
|
||||||
|
// Important to do this here, as new columns could be created after
|
||||||
|
// the user has changed the width.
|
||||||
|
ul.css('width', scope.columnWidth + 'px');
|
||||||
|
|
||||||
|
// Return it, so some li elements can be added
|
||||||
|
return ul;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the width of the columns when user clicks the resize button.
|
||||||
|
function resizeColumn() {
|
||||||
|
element.find('ul').css('width', scope.columnWidth + 'px');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild the DOM associated with this table.
|
||||||
|
function rebuild(domainObjects, rowCount) {
|
||||||
|
var activeColumn;
|
||||||
|
|
||||||
|
// Empty out our cached span elements
|
||||||
|
valueSpans = {};
|
||||||
|
|
||||||
|
// Start with an empty DOM beneath this directive
|
||||||
|
element.html("");
|
||||||
|
|
||||||
|
// Add DOM elements for each domain object being displayed
|
||||||
|
// in this table.
|
||||||
|
domainObjects.forEach(function (object, index) {
|
||||||
|
// Start a new column if we'd run out of room
|
||||||
|
if (index % rowCount === 0) {
|
||||||
|
activeColumn = createColumn(element);
|
||||||
|
}
|
||||||
|
// Add the DOM elements for that object to whichever
|
||||||
|
// column (a `ul` element) is current.
|
||||||
|
createListItem(object, activeColumn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update spans with values, as made available via the
|
||||||
|
// `values` attribute of this directive.
|
||||||
|
function refreshValues() {
|
||||||
|
// Get the available values
|
||||||
|
var values = scope.values || {},
|
||||||
|
classes = scope.classes || {};
|
||||||
|
|
||||||
|
// Populate all spans with those values (or clear
|
||||||
|
// those spans if no value is available)
|
||||||
|
(objects || []).forEach(function (object) {
|
||||||
|
var id = object.getId(),
|
||||||
|
span = valueSpans[id],
|
||||||
|
value;
|
||||||
|
|
||||||
|
if (span) {
|
||||||
|
// Look up the value...
|
||||||
|
value = values[id];
|
||||||
|
// ...and convert to empty string if it's undefined
|
||||||
|
value = value === undefined ? "" : value;
|
||||||
|
span.attr("data-value", value);
|
||||||
|
|
||||||
|
// Update the span
|
||||||
|
span.text(value);
|
||||||
|
span.attr("title", value);
|
||||||
|
span.removeClass(priorClasses[id]);
|
||||||
|
span.addClass(classes[id]);
|
||||||
|
priorClasses[id] = classes[id];
|
||||||
|
}
|
||||||
|
// Also need stale/alert/ok class
|
||||||
|
// on span
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the DOM for this table, if necessary
|
||||||
|
function refreshStructure() {
|
||||||
|
// Only rebuild if number of rows or set of objects
|
||||||
|
// has changed; otherwise, our structure is still valid.
|
||||||
|
if (scope.objects !== objects ||
|
||||||
|
scope.rows !== rows) {
|
||||||
|
|
||||||
|
// Track those values to support future refresh checks
|
||||||
|
objects = scope.objects;
|
||||||
|
rows = scope.rows;
|
||||||
|
|
||||||
|
// Rebuild the DOM
|
||||||
|
rebuild(objects || [], rows || 1);
|
||||||
|
|
||||||
|
// Refresh all data values shown
|
||||||
|
refreshValues();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing the domain objects in use or the number
|
||||||
|
// of rows should trigger a structure change (DOM rebuild)
|
||||||
|
scope.$watch("objects", refreshStructure);
|
||||||
|
scope.$watch("rows", refreshStructure);
|
||||||
|
|
||||||
|
// When the current column width has been changed, resize the column
|
||||||
|
scope.$watch('columnWidth', resizeColumn);
|
||||||
|
|
||||||
|
// When the last-updated time ticks,
|
||||||
|
scope.$watch("updated", refreshValues);
|
||||||
|
|
||||||
|
// Update displayed values when the counter changes.
|
||||||
|
scope.$watch("counter", refreshValues);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return AutoflowTableLinker;
|
||||||
|
}
|
||||||
|
);
|
324
platform/features/autoflow/src/AutoflowTabularController.js
Executable file
324
platform/features/autoflow/src/AutoflowTabularController.js
Executable file
@ -0,0 +1,324 @@
|
|||||||
|
|
||||||
|
define(
|
||||||
|
['moment'],
|
||||||
|
function (moment) {
|
||||||
|
|
||||||
|
var ROW_HEIGHT = 16,
|
||||||
|
SLIDER_HEIGHT = 10,
|
||||||
|
INITIAL_COLUMN_WIDTH = 225,
|
||||||
|
MAX_COLUMN_WIDTH = 525,
|
||||||
|
COLUMN_WIDTH_STEP = 25,
|
||||||
|
DEBOUNCE_INTERVAL = 100,
|
||||||
|
DATE_FORMAT = "YYYY-DDD HH:mm:ss.SSS\\Z",
|
||||||
|
NOT_UPDATED = "No updates",
|
||||||
|
EMPTY_ARRAY = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for supporting the autoflow tabular view.
|
||||||
|
* Implements the all-over logic which drives that view,
|
||||||
|
* mediating between template-provided areas, the included
|
||||||
|
* `mct-autoflow-table` directive, and the underlying
|
||||||
|
* domain object model.
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
function AutflowTabularController(
|
||||||
|
$scope,
|
||||||
|
$timeout,
|
||||||
|
telemetrySubscriber
|
||||||
|
) {
|
||||||
|
var filterValue = "",
|
||||||
|
filterValueLowercase = "",
|
||||||
|
subscription,
|
||||||
|
filteredObjects = [],
|
||||||
|
lastUpdated = {},
|
||||||
|
updateText = NOT_UPDATED,
|
||||||
|
rangeValues = {},
|
||||||
|
classes = {},
|
||||||
|
limits = {},
|
||||||
|
updatePending = false,
|
||||||
|
lastBounce = Number.NEGATIVE_INFINITY,
|
||||||
|
columnWidth = INITIAL_COLUMN_WIDTH,
|
||||||
|
rows = 1,
|
||||||
|
counter = 0;
|
||||||
|
|
||||||
|
// Trigger an update of the displayed table by incrementing
|
||||||
|
// the counter that it watches.
|
||||||
|
function triggerDisplayUpdate() {
|
||||||
|
counter += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether or not an object's name matches the
|
||||||
|
// user-entered filter value.
|
||||||
|
function filterObject(domainObject) {
|
||||||
|
return (domainObject.getModel().name || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.indexOf(filterValueLowercase) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comparator for sorting points back into packet order
|
||||||
|
function compareObject(objectA, objectB) {
|
||||||
|
var indexA = objectA.getModel().index || 0,
|
||||||
|
indexB = objectB.getModel().index || 0;
|
||||||
|
return indexA - indexB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the list of currently-displayed objects; these
|
||||||
|
// will be the subset of currently subscribed-to objects
|
||||||
|
// which match a user-entered filter.
|
||||||
|
function doUpdateFilteredObjects() {
|
||||||
|
// Generate the list
|
||||||
|
filteredObjects = (
|
||||||
|
subscription ?
|
||||||
|
subscription.getTelemetryObjects() :
|
||||||
|
[]
|
||||||
|
).filter(filterObject).sort(compareObject);
|
||||||
|
|
||||||
|
// Clear the pending flag
|
||||||
|
updatePending = false;
|
||||||
|
|
||||||
|
// Track when this occurred, so that we can wait
|
||||||
|
// a whole before updating again.
|
||||||
|
lastBounce = Date.now();
|
||||||
|
|
||||||
|
triggerDisplayUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request an update to the list of current objects; this may
|
||||||
|
// run on a timeout to avoid excessive calls, e.g. while the user
|
||||||
|
// is typing a filter.
|
||||||
|
function updateFilteredObjects() {
|
||||||
|
// Don't do anything if an update is already scheduled
|
||||||
|
if (!updatePending) {
|
||||||
|
if (Date.now() > lastBounce + DEBOUNCE_INTERVAL) {
|
||||||
|
// Update immediately if it's been long enough
|
||||||
|
doUpdateFilteredObjects();
|
||||||
|
} else {
|
||||||
|
// Otherwise, update later, and track that we have
|
||||||
|
// an update pending so that subsequent calls can
|
||||||
|
// be ignored.
|
||||||
|
updatePending = true;
|
||||||
|
$timeout(doUpdateFilteredObjects, DEBOUNCE_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track the latest data values for this domain object
|
||||||
|
function recordData(telemetryObject) {
|
||||||
|
// Get latest domain/range values for this object.
|
||||||
|
var id = telemetryObject.getId(),
|
||||||
|
domainValue = subscription.getDomainValue(telemetryObject),
|
||||||
|
rangeValue = subscription.getRangeValue(telemetryObject);
|
||||||
|
|
||||||
|
// Track the most recent timestamp change observed...
|
||||||
|
if (domainValue !== undefined && domainValue !== lastUpdated[id]) {
|
||||||
|
lastUpdated[id] = domainValue;
|
||||||
|
// ... and update the displayable text for that timestamp
|
||||||
|
updateText = isNaN(domainValue) ? "" :
|
||||||
|
moment.utc(domainValue).format(DATE_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store data values into the rangeValues structure, which
|
||||||
|
// will be used to populate the table itself.
|
||||||
|
// Note that we want full precision here.
|
||||||
|
rangeValues[id] = rangeValue;
|
||||||
|
|
||||||
|
// Update limit states as well
|
||||||
|
classes[id] = limits[id] && (limits[id].evaluate({
|
||||||
|
// This relies on external knowledge that the
|
||||||
|
// range value of a telemetry point is encoded
|
||||||
|
// in its datum as "value."
|
||||||
|
value: rangeValue
|
||||||
|
}) || {}).cssClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Look at telemetry objects from the subscription; this is watched
|
||||||
|
// to detect changes from the subscription.
|
||||||
|
function subscribedTelemetry() {
|
||||||
|
return subscription ?
|
||||||
|
subscription.getTelemetryObjects() : EMPTY_ARRAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the data values which will be used to populate the table
|
||||||
|
function updateValues() {
|
||||||
|
subscribedTelemetry().forEach(recordData);
|
||||||
|
triggerDisplayUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter-setter function for user-entered filter text.
|
||||||
|
function filter(value) {
|
||||||
|
// If value was specified, we're a setter
|
||||||
|
if (value !== undefined) {
|
||||||
|
// Store the new value
|
||||||
|
filterValue = value;
|
||||||
|
filterValueLowercase = value.toLowerCase();
|
||||||
|
// Change which objects appear in the table
|
||||||
|
updateFilteredObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always act as a getter
|
||||||
|
return filterValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the bounds (width and height) of this view;
|
||||||
|
// called from the mct-resize directive. Recalculates how
|
||||||
|
// many rows should appear in the contained table.
|
||||||
|
function setBounds(bounds) {
|
||||||
|
var availableSpace = bounds.height - SLIDER_HEIGHT;
|
||||||
|
rows = Math.max(1, Math.floor(availableSpace / ROW_HEIGHT));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment the current column width, up to the defined maximum.
|
||||||
|
// When the max is hit, roll back to the default.
|
||||||
|
function increaseColumnWidth() {
|
||||||
|
columnWidth += COLUMN_WIDTH_STEP;
|
||||||
|
// Cycle down to the initial width instead of exceeding max
|
||||||
|
columnWidth = columnWidth > MAX_COLUMN_WIDTH ?
|
||||||
|
INITIAL_COLUMN_WIDTH : columnWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get displayable text for last-updated value
|
||||||
|
function updated() {
|
||||||
|
return updateText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe, if a subscription is active.
|
||||||
|
function releaseSubscription() {
|
||||||
|
if (subscription) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
subscription = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update set of telemetry objects managed by this view
|
||||||
|
function updateTelemetryObjects(telemetryObjects) {
|
||||||
|
updateFilteredObjects();
|
||||||
|
limits = {};
|
||||||
|
telemetryObjects.forEach(function (telemetryObject) {
|
||||||
|
var id = telemetryObject.getId();
|
||||||
|
limits[id] = telemetryObject.getCapability('limit');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a subscription for the represented domain object.
|
||||||
|
// This will resolve capability delegation as necessary.
|
||||||
|
function makeSubscription(domainObject) {
|
||||||
|
// Unsubscribe, if there is an existing subscription
|
||||||
|
releaseSubscription();
|
||||||
|
|
||||||
|
// Clear updated timestamp
|
||||||
|
lastUpdated = {};
|
||||||
|
updateText = NOT_UPDATED;
|
||||||
|
|
||||||
|
// Create a new subscription; telemetrySubscriber gets
|
||||||
|
// to do the meaningful work here.
|
||||||
|
subscription = domainObject && telemetrySubscriber.subscribe(
|
||||||
|
domainObject,
|
||||||
|
updateValues
|
||||||
|
);
|
||||||
|
|
||||||
|
// Our set of in-view telemetry objects may have changed,
|
||||||
|
// so update the set that is being passed down to the table.
|
||||||
|
updateFilteredObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for changes to the set of objects which have telemetry
|
||||||
|
$scope.$watch(subscribedTelemetry, updateTelemetryObjects);
|
||||||
|
|
||||||
|
// Watch for the represented domainObject (this field will
|
||||||
|
// be populated by mct-representation)
|
||||||
|
$scope.$watch("domainObject", makeSubscription);
|
||||||
|
|
||||||
|
// Make sure we unsubscribe when this view is destroyed.
|
||||||
|
$scope.$on("$destroy", releaseSubscription);
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Get the number of rows which should be shown in this table.
|
||||||
|
* @return {number} the number of rows to show
|
||||||
|
*/
|
||||||
|
getRows: function () {
|
||||||
|
return rows;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get the objects which should currently be displayed in
|
||||||
|
* this table. This will be watched, so the return value
|
||||||
|
* should be stable when this list is unchanging. Only
|
||||||
|
* objects which match the user-entered filter value should
|
||||||
|
* be returned here.
|
||||||
|
* @return {DomainObject[]} the domain objects to include in
|
||||||
|
* this table.
|
||||||
|
*/
|
||||||
|
getTelemetryObjects: function () {
|
||||||
|
return filteredObjects;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Set the bounds (width/height) of this autoflow tabular view.
|
||||||
|
* The template must ensure that these bounds are tracked on
|
||||||
|
* the table area only.
|
||||||
|
* @param bounds the bounds; and object with `width` and
|
||||||
|
* `height` properties, both as numbers, in pixels.
|
||||||
|
*/
|
||||||
|
setBounds: setBounds,
|
||||||
|
/**
|
||||||
|
* Increments the width of the autoflow column.
|
||||||
|
* Setting does not yet persist.
|
||||||
|
*/
|
||||||
|
increaseColumnWidth: increaseColumnWidth,
|
||||||
|
/**
|
||||||
|
* Get-or-set the user-supplied filter value.
|
||||||
|
* @param {string} [value] the new filter value; omit to use
|
||||||
|
* as a getter
|
||||||
|
* @returns {string} the user-supplied filter value
|
||||||
|
*/
|
||||||
|
filter: filter,
|
||||||
|
/**
|
||||||
|
* Get all range values for use in this table. These will be
|
||||||
|
* returned as an object of key-value pairs, where keys are
|
||||||
|
* domain object IDs, and values are the most recently observed
|
||||||
|
* data values associated with those objects, formatted for
|
||||||
|
* display.
|
||||||
|
* @returns {object.<string,string>} most recent values
|
||||||
|
*/
|
||||||
|
rangeValues: function () {
|
||||||
|
return rangeValues;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get CSS classes to apply to specific rows, representing limit
|
||||||
|
* states and/or stale states. These are returned as key-value
|
||||||
|
* pairs where keys are domain object IDs, and values are CSS
|
||||||
|
* classes to display for domain objects with those IDs.
|
||||||
|
* @returns {object.<string,string>} CSS classes
|
||||||
|
*/
|
||||||
|
classes: function () {
|
||||||
|
return classes;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get the "last updated" text for this view; this will be
|
||||||
|
* the most recent timestamp observed for any telemetry-
|
||||||
|
* providing object, formatted for display.
|
||||||
|
* @returns {string} the time of the most recent update
|
||||||
|
*/
|
||||||
|
updated: updated,
|
||||||
|
/**
|
||||||
|
* Get the current column width, in pixels.
|
||||||
|
* @returns {number} column width
|
||||||
|
*/
|
||||||
|
columnWidth: function () {
|
||||||
|
return columnWidth;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Keep a counter and increment this whenever the display
|
||||||
|
* should be updated; this will be watched by the
|
||||||
|
* `mct-autoflow-table`.
|
||||||
|
* @returns {number} a counter value
|
||||||
|
*/
|
||||||
|
counter: function () {
|
||||||
|
return counter;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return AutflowTabularController;
|
||||||
|
}
|
||||||
|
);
|
60
platform/features/autoflow/src/MCTAutoflowTable.js
Executable file
60
platform/features/autoflow/src/MCTAutoflowTable.js
Executable file
@ -0,0 +1,60 @@
|
|||||||
|
|
||||||
|
define(
|
||||||
|
["./AutoflowTableLinker"],
|
||||||
|
function (AutoflowTableLinker) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `mct-autoflow-table` directive specifically supports
|
||||||
|
* autoflow tabular views; it is not intended for use outside
|
||||||
|
* of that view.
|
||||||
|
*
|
||||||
|
* This directive is responsible for creating the structure
|
||||||
|
* of the table in this view, and for updating its values.
|
||||||
|
* While this is achievable using a regular Angular template,
|
||||||
|
* this is undesirable from the perspective of performance
|
||||||
|
* due to the number of watches that can be involved for large
|
||||||
|
* tables. Instead, this directive will maintain a small number
|
||||||
|
* of watches, rebuilding table structure only when necessary,
|
||||||
|
* and updating displayed values in the more common case of
|
||||||
|
* new data arriving.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
function MCTAutoflowTable() {
|
||||||
|
return {
|
||||||
|
// Only applicable at the element level
|
||||||
|
restrict: "E",
|
||||||
|
|
||||||
|
// The link function; handles DOM update/manipulation
|
||||||
|
link: AutoflowTableLinker,
|
||||||
|
|
||||||
|
// Parameters to pass from attributes into scope
|
||||||
|
scope: {
|
||||||
|
// Set of domain objects to show in the table
|
||||||
|
objects: "=",
|
||||||
|
|
||||||
|
// Values for those objects, by ID
|
||||||
|
values: "=",
|
||||||
|
|
||||||
|
// CSS classes to show for objects, by ID
|
||||||
|
classes: "=",
|
||||||
|
|
||||||
|
// Number of rows to show before autoflowing
|
||||||
|
rows: "=",
|
||||||
|
|
||||||
|
// Time of last update; watched to refresh values
|
||||||
|
updated: "=",
|
||||||
|
|
||||||
|
// Current width of the autoflow column
|
||||||
|
columnWidth: "=",
|
||||||
|
|
||||||
|
// A counter used to trigger display updates
|
||||||
|
counter: "="
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return MCTAutoflowTable;
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
178
platform/features/autoflow/test/AutoflowTableLinkerSpec.js
Executable file
178
platform/features/autoflow/test/AutoflowTableLinkerSpec.js
Executable file
@ -0,0 +1,178 @@
|
|||||||
|
|
||||||
|
define(
|
||||||
|
["../src/AutoflowTableLinker"],
|
||||||
|
function (AutoflowTableLinker) {
|
||||||
|
|
||||||
|
describe("The mct-autoflow-table linker", function () {
|
||||||
|
var cachedAngular,
|
||||||
|
mockAngular,
|
||||||
|
mockScope,
|
||||||
|
mockElement,
|
||||||
|
mockElements,
|
||||||
|
linker;
|
||||||
|
|
||||||
|
// Utility function to generate more mock elements
|
||||||
|
function createMockElement(html) {
|
||||||
|
var mockEl = jasmine.createSpyObj(
|
||||||
|
"element-" + html,
|
||||||
|
[
|
||||||
|
"append",
|
||||||
|
"addClass",
|
||||||
|
"removeClass",
|
||||||
|
"text",
|
||||||
|
"attr",
|
||||||
|
"html",
|
||||||
|
"css",
|
||||||
|
"find"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
mockEl.testHtml = html;
|
||||||
|
mockEl.append.andReturn(mockEl);
|
||||||
|
mockElements.push(mockEl);
|
||||||
|
return mockEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockDomainObject(id) {
|
||||||
|
var mockDomainObject = jasmine.createSpyObj(
|
||||||
|
"domainObject-" + id,
|
||||||
|
["getId", "getModel"]
|
||||||
|
);
|
||||||
|
mockDomainObject.getId.andReturn(id);
|
||||||
|
mockDomainObject.getModel.andReturn({name: id.toUpperCase()});
|
||||||
|
return mockDomainObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fireWatch(watchExpression, value) {
|
||||||
|
mockScope.$watch.calls.forEach(function (call) {
|
||||||
|
if (call.args[0] === watchExpression) {
|
||||||
|
call.args[1](value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoflowTableLinker accesses Angular in the global
|
||||||
|
// scope, since it is not injectable; we simulate that
|
||||||
|
// here by adding/removing it to/from the window object.
|
||||||
|
beforeEach(function () {
|
||||||
|
mockElements = [];
|
||||||
|
|
||||||
|
mockAngular = jasmine.createSpyObj("angular", ["element"]);
|
||||||
|
mockScope = jasmine.createSpyObj("scope", ["$watch"]);
|
||||||
|
mockElement = createMockElement('<div>');
|
||||||
|
|
||||||
|
mockAngular.element.andCallFake(createMockElement);
|
||||||
|
|
||||||
|
if (window.angular !== undefined) {
|
||||||
|
cachedAngular = window.angular;
|
||||||
|
}
|
||||||
|
window.angular = mockAngular;
|
||||||
|
|
||||||
|
linker = new AutoflowTableLinker(mockScope, mockElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
if (cachedAngular !== undefined) {
|
||||||
|
window.angular = cachedAngular;
|
||||||
|
} else {
|
||||||
|
delete window.angular;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("watches for changes in inputs", function () {
|
||||||
|
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||||
|
"objects",
|
||||||
|
jasmine.any(Function)
|
||||||
|
);
|
||||||
|
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||||
|
"rows",
|
||||||
|
jasmine.any(Function)
|
||||||
|
);
|
||||||
|
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||||
|
"counter",
|
||||||
|
jasmine.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("changes structure when domain objects change", function () {
|
||||||
|
// Set up scope
|
||||||
|
mockScope.rows = 4;
|
||||||
|
mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f']
|
||||||
|
.map(createMockDomainObject);
|
||||||
|
|
||||||
|
// Fire an update to the set of objects
|
||||||
|
fireWatch("objects");
|
||||||
|
|
||||||
|
// Should have rebuilt with two columns of
|
||||||
|
// four and two rows each; first, by clearing...
|
||||||
|
expect(mockElement.html).toHaveBeenCalledWith("");
|
||||||
|
|
||||||
|
// Should have appended two columns...
|
||||||
|
expect(mockElement.append.calls.length).toEqual(2);
|
||||||
|
|
||||||
|
// ...which should have received two and four rows each
|
||||||
|
expect(mockElement.append.calls[0].args[0].append.calls.length)
|
||||||
|
.toEqual(4);
|
||||||
|
expect(mockElement.append.calls[1].args[0].append.calls.length)
|
||||||
|
.toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates values", function () {
|
||||||
|
var mockSpans;
|
||||||
|
|
||||||
|
mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f']
|
||||||
|
.map(createMockDomainObject);
|
||||||
|
mockScope.values = { a: 0 };
|
||||||
|
|
||||||
|
// Fire an update to the set of values
|
||||||
|
fireWatch("objects");
|
||||||
|
fireWatch("updated");
|
||||||
|
|
||||||
|
// Get all created spans
|
||||||
|
mockSpans = mockElements.filter(function (mockElem) {
|
||||||
|
return mockElem.testHtml === '<span>';
|
||||||
|
});
|
||||||
|
|
||||||
|
// First span should be a, should have gotten this value.
|
||||||
|
// This test detects, in particular, WTD-749
|
||||||
|
expect(mockSpans[0].text).toHaveBeenCalledWith('A');
|
||||||
|
expect(mockSpans[1].text).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("listens for changes in column width", function () {
|
||||||
|
var mockUL = createMockElement("<ul>");
|
||||||
|
mockElement.find.andReturn(mockUL);
|
||||||
|
mockScope.columnWidth = 200;
|
||||||
|
fireWatch("columnWidth", mockScope.columnWidth);
|
||||||
|
expect(mockUL.css).toHaveBeenCalledWith("width", "200px");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates CSS classes", function () {
|
||||||
|
var mockSpans;
|
||||||
|
|
||||||
|
mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f']
|
||||||
|
.map(createMockDomainObject);
|
||||||
|
mockScope.values = { a: "a value to find" };
|
||||||
|
mockScope.classes = { a: 'class-a' };
|
||||||
|
|
||||||
|
// Fire an update to the set of values
|
||||||
|
fireWatch("objects");
|
||||||
|
fireWatch("updated");
|
||||||
|
|
||||||
|
// Figure out which span holds the relevant value...
|
||||||
|
mockSpans = mockElements.filter(function (mockElem) {
|
||||||
|
return mockElem.testHtml === '<span>';
|
||||||
|
}).filter(function (mockSpan) {
|
||||||
|
var attrCalls = mockSpan.attr.calls;
|
||||||
|
return attrCalls.some(function (call) {
|
||||||
|
return call.args[0] === 'title' &&
|
||||||
|
call.args[1] === mockScope.values.a;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ...and make sure it also has had its class applied
|
||||||
|
expect(mockSpans[0].addClass)
|
||||||
|
.toHaveBeenCalledWith(mockScope.classes.a);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
341
platform/features/autoflow/test/AutoflowTabularControllerSpec.js
Executable file
341
platform/features/autoflow/test/AutoflowTabularControllerSpec.js
Executable file
@ -0,0 +1,341 @@
|
|||||||
|
|
||||||
|
define(
|
||||||
|
["../src/AutoflowTabularController"],
|
||||||
|
function (AutoflowTabularController) {
|
||||||
|
|
||||||
|
describe("The autoflow tabular controller", function () {
|
||||||
|
var mockScope,
|
||||||
|
mockTimeout,
|
||||||
|
mockSubscriber,
|
||||||
|
mockDomainObject,
|
||||||
|
mockSubscription,
|
||||||
|
controller;
|
||||||
|
|
||||||
|
// Fire watches that are registered as functions.
|
||||||
|
function fireFnWatches() {
|
||||||
|
mockScope.$watch.calls.forEach(function (call) {
|
||||||
|
if (typeof call.args[0] === 'function') {
|
||||||
|
call.args[1](call.args[0]());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
mockScope = jasmine.createSpyObj(
|
||||||
|
"$scope",
|
||||||
|
["$on", "$watch"]
|
||||||
|
);
|
||||||
|
mockTimeout = jasmine.createSpy("$timeout");
|
||||||
|
mockSubscriber = jasmine.createSpyObj(
|
||||||
|
"telemetrySubscriber",
|
||||||
|
["subscribe"]
|
||||||
|
);
|
||||||
|
mockDomainObject = jasmine.createSpyObj(
|
||||||
|
"domainObject",
|
||||||
|
["getId", "getModel", "getCapability"]
|
||||||
|
);
|
||||||
|
mockSubscription = jasmine.createSpyObj(
|
||||||
|
"subscription",
|
||||||
|
[
|
||||||
|
"unsubscribe",
|
||||||
|
"getTelemetryObjects",
|
||||||
|
"getDomainValue",
|
||||||
|
"getRangeValue"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
mockSubscriber.subscribe.andReturn(mockSubscription);
|
||||||
|
mockDomainObject.getModel.andReturn({name: "something"});
|
||||||
|
|
||||||
|
controller = new AutoflowTabularController(
|
||||||
|
mockScope,
|
||||||
|
mockTimeout,
|
||||||
|
mockSubscriber
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("listens for the represented domain object", function () {
|
||||||
|
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||||
|
"domainObject",
|
||||||
|
jasmine.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("provides a getter-setter function for filtering", function () {
|
||||||
|
expect(controller.filter()).toEqual("");
|
||||||
|
controller.filter("something");
|
||||||
|
expect(controller.filter()).toEqual("something");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tracks bounds and adjust number of rows accordingly", function () {
|
||||||
|
// Rows are 15px high, and need room for an 10px slider
|
||||||
|
controller.setBounds({ width: 700, height: 120 });
|
||||||
|
expect(controller.getRows()).toEqual(6); // 110 usable height / 16px
|
||||||
|
controller.setBounds({ width: 700, height: 240 });
|
||||||
|
expect(controller.getRows()).toEqual(14); // 230 usable height / 16px
|
||||||
|
});
|
||||||
|
|
||||||
|
it("subscribes to a represented object's telemetry", function () {
|
||||||
|
// Set up subscription, scope
|
||||||
|
mockSubscription.getTelemetryObjects
|
||||||
|
.andReturn([mockDomainObject]);
|
||||||
|
mockScope.domainObject = mockDomainObject;
|
||||||
|
|
||||||
|
// Invoke the watcher with represented domain object
|
||||||
|
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||||
|
|
||||||
|
// Should have subscribed to it
|
||||||
|
expect(mockSubscriber.subscribe).toHaveBeenCalledWith(
|
||||||
|
mockDomainObject,
|
||||||
|
jasmine.any(Function)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should report objects as reported from subscription
|
||||||
|
expect(controller.getTelemetryObjects())
|
||||||
|
.toEqual([mockDomainObject]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("releases subscriptions on destroy", function () {
|
||||||
|
// Set up subscription...
|
||||||
|
mockSubscription.getTelemetryObjects
|
||||||
|
.andReturn([mockDomainObject]);
|
||||||
|
mockScope.domainObject = mockDomainObject;
|
||||||
|
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||||
|
|
||||||
|
// Verify precondition
|
||||||
|
expect(mockSubscription.unsubscribe).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Make sure we're listening for $destroy
|
||||||
|
expect(mockScope.$on).toHaveBeenCalledWith(
|
||||||
|
"$destroy",
|
||||||
|
jasmine.any(Function)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fire a destroy event
|
||||||
|
mockScope.$on.mostRecentCall.args[1]();
|
||||||
|
|
||||||
|
// Should have unsubscribed
|
||||||
|
expect(mockSubscription.unsubscribe).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("presents latest values and latest update state", function () {
|
||||||
|
// Make sure values are available
|
||||||
|
mockSubscription.getDomainValue.andReturn(402654321123);
|
||||||
|
mockSubscription.getRangeValue.andReturn(789);
|
||||||
|
mockDomainObject.getId.andReturn('testId');
|
||||||
|
|
||||||
|
// Set up subscription...
|
||||||
|
mockSubscription.getTelemetryObjects
|
||||||
|
.andReturn([mockDomainObject]);
|
||||||
|
mockScope.domainObject = mockDomainObject;
|
||||||
|
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||||
|
|
||||||
|
// Fire subscription callback
|
||||||
|
mockSubscriber.subscribe.mostRecentCall.args[1]();
|
||||||
|
|
||||||
|
// ...and exposed the results for template to consume
|
||||||
|
expect(controller.updated()).toEqual("1982-278 08:25:21.123Z");
|
||||||
|
expect(controller.rangeValues().testId).toEqual(789);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sorts domain objects by index", function () {
|
||||||
|
var testIndexes = { a: 2, b: 1, c: 3, d: 0 },
|
||||||
|
mockDomainObjects = Object.keys(testIndexes).sort().map(function (id) {
|
||||||
|
var mockDomainObj = jasmine.createSpyObj(
|
||||||
|
"domainObject",
|
||||||
|
["getId", "getModel"]
|
||||||
|
);
|
||||||
|
|
||||||
|
mockDomainObj.getId.andReturn(id);
|
||||||
|
mockDomainObj.getModel.andReturn({ index: testIndexes[id] });
|
||||||
|
|
||||||
|
return mockDomainObj;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expose those domain objects...
|
||||||
|
mockSubscription.getTelemetryObjects.andReturn(mockDomainObjects);
|
||||||
|
mockScope.domainObject = mockDomainObject;
|
||||||
|
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||||
|
|
||||||
|
// Fire subscription callback
|
||||||
|
mockSubscriber.subscribe.mostRecentCall.args[1]();
|
||||||
|
|
||||||
|
// Controller should expose same objects, but sorted by index from model
|
||||||
|
expect(controller.getTelemetryObjects()).toEqual([
|
||||||
|
mockDomainObjects[3], // d, index=0
|
||||||
|
mockDomainObjects[1], // b, index=1
|
||||||
|
mockDomainObjects[0], // a, index=2
|
||||||
|
mockDomainObjects[2] // c, index=3
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses a timeout to throttle update", function () {
|
||||||
|
// Set up subscription...
|
||||||
|
mockSubscription.getTelemetryObjects
|
||||||
|
.andReturn([mockDomainObject]);
|
||||||
|
mockScope.domainObject = mockDomainObject;
|
||||||
|
|
||||||
|
// Set the object in view; should not need a timeout
|
||||||
|
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||||
|
expect(mockTimeout.calls.length).toEqual(0);
|
||||||
|
|
||||||
|
// Next call should schedule an update on a timeout
|
||||||
|
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||||
|
expect(mockTimeout.calls.length).toEqual(1);
|
||||||
|
|
||||||
|
// ...but this last one should not, since existing
|
||||||
|
// timeout will cover it
|
||||||
|
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||||
|
expect(mockTimeout.calls.length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows changing column width", function () {
|
||||||
|
var initialWidth = controller.columnWidth();
|
||||||
|
controller.increaseColumnWidth();
|
||||||
|
expect(controller.columnWidth()).toBeGreaterThan(initialWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter", function () {
|
||||||
|
var doFilter,
|
||||||
|
filteredObjects,
|
||||||
|
filteredObjectNames;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
var telemetryObjects,
|
||||||
|
updateFilteredObjects;
|
||||||
|
|
||||||
|
telemetryObjects = [
|
||||||
|
'DEF123',
|
||||||
|
'abc789',
|
||||||
|
'456abc',
|
||||||
|
'4ab3cdef',
|
||||||
|
'hjs[12].*(){}^\\'
|
||||||
|
].map(function (objectName, index) {
|
||||||
|
var mockTelemetryObject = jasmine.createSpyObj(
|
||||||
|
objectName,
|
||||||
|
["getId", "getModel"]
|
||||||
|
);
|
||||||
|
|
||||||
|
mockTelemetryObject.getId.andReturn(objectName);
|
||||||
|
mockTelemetryObject.getModel.andReturn({
|
||||||
|
name: objectName,
|
||||||
|
index: index
|
||||||
|
});
|
||||||
|
|
||||||
|
return mockTelemetryObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
mockSubscription
|
||||||
|
.getTelemetryObjects
|
||||||
|
.andReturn(telemetryObjects);
|
||||||
|
|
||||||
|
// Trigger domainObject change to create subscription.
|
||||||
|
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||||
|
|
||||||
|
updateFilteredObjects = function () {
|
||||||
|
filteredObjects = controller.getTelemetryObjects();
|
||||||
|
filteredObjectNames = filteredObjects.map(function (o) {
|
||||||
|
return o.getModel().name;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
doFilter = function (term) {
|
||||||
|
controller.filter(term);
|
||||||
|
// Filter is debounced so we have to force it to occur.
|
||||||
|
mockTimeout.mostRecentCall.args[0]();
|
||||||
|
updateFilteredObjects();
|
||||||
|
};
|
||||||
|
|
||||||
|
updateFilteredObjects();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("initially shows all objects", function () {
|
||||||
|
expect(filteredObjectNames).toEqual([
|
||||||
|
'DEF123',
|
||||||
|
'abc789',
|
||||||
|
'456abc',
|
||||||
|
'4ab3cdef',
|
||||||
|
'hjs[12].*(){}^\\'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("by blank string matches all objects", function () {
|
||||||
|
doFilter('');
|
||||||
|
expect(filteredObjectNames).toEqual([
|
||||||
|
'DEF123',
|
||||||
|
'abc789',
|
||||||
|
'456abc',
|
||||||
|
'4ab3cdef',
|
||||||
|
'hjs[12].*(){}^\\'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exactly matches an object name", function () {
|
||||||
|
doFilter('4ab3cdef');
|
||||||
|
expect(filteredObjectNames).toEqual(['4ab3cdef']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("partially matches object names", function () {
|
||||||
|
doFilter('abc');
|
||||||
|
expect(filteredObjectNames).toEqual([
|
||||||
|
'abc789',
|
||||||
|
'456abc'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches case insensitive names", function () {
|
||||||
|
doFilter('def');
|
||||||
|
expect(filteredObjectNames).toEqual([
|
||||||
|
'DEF123',
|
||||||
|
'4ab3cdef'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works as expected with special characters", function () {
|
||||||
|
doFilter('[12]');
|
||||||
|
expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']);
|
||||||
|
doFilter('.*');
|
||||||
|
expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']);
|
||||||
|
doFilter('.*()');
|
||||||
|
expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']);
|
||||||
|
doFilter('.*?');
|
||||||
|
expect(filteredObjectNames).toEqual([]);
|
||||||
|
doFilter('.+');
|
||||||
|
expect(filteredObjectNames).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes CSS classes from limits", function () {
|
||||||
|
var id = mockDomainObject.getId(),
|
||||||
|
testClass = "some-css-class",
|
||||||
|
mockLimitCapability =
|
||||||
|
jasmine.createSpyObj('limit', ['evaluate']);
|
||||||
|
|
||||||
|
mockDomainObject.getCapability.andCallFake(function (key) {
|
||||||
|
return key === 'limit' && mockLimitCapability;
|
||||||
|
});
|
||||||
|
mockLimitCapability.evaluate
|
||||||
|
.andReturn({ cssClass: testClass });
|
||||||
|
|
||||||
|
mockSubscription.getTelemetryObjects
|
||||||
|
.andReturn([mockDomainObject]);
|
||||||
|
|
||||||
|
fireFnWatches();
|
||||||
|
mockSubscriber.subscribe.mostRecentCall.args[1]();
|
||||||
|
|
||||||
|
expect(controller.classes()[id]).toEqual(testClass);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes a counter that changes with each update", function () {
|
||||||
|
var i, prior;
|
||||||
|
|
||||||
|
for (i = 0; i < 10; i += 1) {
|
||||||
|
prior = controller.counter();
|
||||||
|
expect(controller.counter()).toEqual(prior);
|
||||||
|
mockSubscriber.subscribe.mostRecentCall.args[1]();
|
||||||
|
expect(controller.counter()).not.toEqual(prior);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
39
platform/features/autoflow/test/MCTAutoflowTableSpec.js
Executable file
39
platform/features/autoflow/test/MCTAutoflowTableSpec.js
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
define(
|
||||||
|
["../src/MCTAutoflowTable"],
|
||||||
|
function (MCTAutoflowTable) {
|
||||||
|
|
||||||
|
describe("The mct-autoflow-table directive", function () {
|
||||||
|
var mctAutoflowTable;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
mctAutoflowTable = new MCTAutoflowTable();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Real functionality is contained/tested in the linker,
|
||||||
|
// so just check to make sure we're exposing the directive
|
||||||
|
// appropriately.
|
||||||
|
it("is applicable at the element level", function () {
|
||||||
|
expect(mctAutoflowTable.restrict).toEqual("E");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("two-ways binds needed scope variables", function () {
|
||||||
|
expect(mctAutoflowTable.scope).toEqual({
|
||||||
|
objects: "=",
|
||||||
|
values: "=",
|
||||||
|
rows: "=",
|
||||||
|
updated: "=",
|
||||||
|
classes: "=",
|
||||||
|
columnWidth: "=",
|
||||||
|
counter: "="
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("provides a link function", function () {
|
||||||
|
expect(mctAutoflowTable.link).toEqual(jasmine.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
@ -23,11 +23,13 @@
|
|||||||
define([
|
define([
|
||||||
'lodash',
|
'lodash',
|
||||||
'../../platform/features/conductor/utcTimeSystem/src/UTCTimeSystem',
|
'../../platform/features/conductor/utcTimeSystem/src/UTCTimeSystem',
|
||||||
'../../example/generator/plugin'
|
'../../example/generator/plugin',
|
||||||
|
'../../platform/features/autoflow/plugin'
|
||||||
], function (
|
], function (
|
||||||
_,
|
_,
|
||||||
UTCTimeSystem,
|
UTCTimeSystem,
|
||||||
GeneratorPlugin
|
GeneratorPlugin,
|
||||||
|
AutoflowPlugin
|
||||||
) {
|
) {
|
||||||
var bundleMap = {
|
var bundleMap = {
|
||||||
CouchDB: 'platform/persistence/couch',
|
CouchDB: 'platform/persistence/couch',
|
||||||
@ -55,6 +57,10 @@ define([
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
plugins.AutoflowView = function () {
|
||||||
|
return AutoflowPlugin;
|
||||||
|
};
|
||||||
|
|
||||||
var conductorInstalled = false;
|
var conductorInstalled = false;
|
||||||
|
|
||||||
plugins.Conductor = function (options) {
|
plugins.Conductor = function (options) {
|
||||||
|
Loading…
Reference in New Issue
Block a user