Compare commits

...

23 Commits

Author SHA1 Message Date
580a4e52b5 Merge branch 'api-tutorials' into api-type-driven 2016-06-07 14:01:08 -07:00
9c4e17bfab [Tutorials] Add telemetry tutorial 2016-06-07 13:14:36 -07:00
d3e5d95d6b [Tutorials] Add example server 2016-06-07 13:00:38 -07:00
c70793ac2d [Tutorials] Add remainder of bargraph 2016-06-07 12:55:29 -07:00
a6ef1d3423 [Tutorials] Add Bar Graph tutorial 2016-06-07 12:49:38 -07:00
4ca2f51d5e [API] Use subclass style 2016-05-27 17:08:25 -07:00
86ac80ddbd [API] Persist mutations 2016-05-27 16:56:08 -07:00
0525ba6b0b [API] Check/uncheck todos 2016-05-27 16:55:10 -07:00
a79e958ffc [API] Show tasks from todo 2016-05-27 16:46:06 -07:00
03cb0ccb57 [API] Get View to render 2016-05-27 16:36:55 -07:00
7205faa6bb [API] Add adapter bundle 2016-05-27 16:27:47 -07:00
136f2ae785 [API] Add MCTView directive as an adapter 2016-05-27 16:19:20 -07:00
a07e2fb8e5 [API] Implement View 2016-05-27 16:08:43 -07:00
55b531bdeb [API] Sketch in view instantiation 2016-05-27 15:49:16 -07:00
7ece5897e8 [API] Begin adding View 2016-05-27 15:31:54 -07:00
a29c7a6eab [API] Deangularize todo templates 2016-05-27 13:46:30 -07:00
c4fec1af6a [API] Move type toward a newer API 2016-05-27 13:31:30 -07:00
a6996df3df [API] Begin moving out type 2016-05-27 13:17:16 -07:00
0c660238f2 [API] Add MCT class 2016-05-27 11:49:43 -07:00
b73b824e55 [API] Add EventEmitter dep 2016-05-27 11:45:59 -07:00
1954d98628 [Tutorials] Remove diff markings in TodoController 2016-05-27 11:30:53 -07:00
7aa034ce23 Add todo tutorial 2016-05-26 16:05:38 -07:00
385dc5d298 Begin adding tutorials 2016-05-26 15:36:09 -07:00
27 changed files with 1269 additions and 9 deletions

View File

@ -18,6 +18,7 @@
"node-uuid": "^1.4.7",
"comma-separated-values": "^3.6.4",
"FileSaver.js": "^0.0.2",
"zepto": "^1.1.6"
"zepto": "^1.1.6",
"eventemitter3": "^1.2.0"
}
}

View File

@ -31,10 +31,15 @@
<script type="text/javascript">
require(['main'], function (mct) {
require([
'./tutorials/todo/todo',
'./tutorials/todo/bundle',
'./example/imagery/bundle',
'./example/eventGenerator/bundle',
'./example/generator/bundle'
], mct.run.bind(mct));
], function (todoPlugin) {
todoPlugin(mct);
mct.start();
})
});
</script>
<link rel="stylesheet" href="platform/commonUI/general/res/css/startup-base.css">

24
main.js
View File

@ -28,6 +28,7 @@ requirejs.config({
"angular-route": "bower_components/angular-route/angular-route.min",
"csv": "bower_components/comma-separated-values/csv.min",
"es6-promise": "bower_components/es6-promise/promise.min",
"EventEmitter": "bower_components/eventemitter3/index",
"moment": "bower_components/moment/moment",
"moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format",
"saveAs": "bower_components/FileSaver.js/FileSaver.min",
@ -43,6 +44,9 @@ requirejs.config({
"angular-route": {
"deps": ["angular"]
},
"EventEmitter": {
"exports": "EventEmitter"
},
"moment-duration-format": {
"deps": ["moment"]
},
@ -58,6 +62,9 @@ requirejs.config({
define([
'./platform/framework/src/Main',
'legacyRegistry',
'./src/MCT',
'./src/adapter/bundle',
'./platform/framework/bundle',
'./platform/core/bundle',
@ -93,11 +100,14 @@ define([
'./platform/search/bundle',
'./platform/status/bundle',
'./platform/commonUI/regions/bundle'
], function (Main, legacyRegistry) {
return {
legacyRegistry: legacyRegistry,
run: function () {
return new Main().run(legacyRegistry);
}
};
], function (Main, legacyRegistry, MCT) {
var mct = new MCT();
mct.legacyRegistry = legacyRegistry;
mct.run = mct.start;
mct.on('start', function () {
return new Main().run(legacyRegistry);
});
return mct;
});

70
src/MCT.js Normal file
View File

@ -0,0 +1,70 @@
define([
'EventEmitter',
'legacyRegistry',
'./api/api'
], function (EventEmitter, legacyRegistry, api) {
function MCT() {
EventEmitter.call(this);
this.legacyBundle = { extensions: {} };
}
MCT.prototype = Object.create(EventEmitter.prototype);
Object.keys(api).forEach(function (k) {
MCT.prototype[k] = api[k];
});
MCT.prototype.MCT = MCT;
MCT.prototype.type = function (key, type) {
var legacyDef = type.toLegacyDefinition();
legacyDef.key = key;
this.legacyBundle.extensions.types =
this.legacyBundle.extensions.types || [];
this.legacyBundle.extensions.types.push(legacyDef);
var viewFactory = type.view(this.regions.main);
if (viewFactory) {
var viewKey = key + "." + this.regions.main;
this.legacyBundle.extensions.views =
this.legacyBundle.extensions.views || [];
this.legacyBundle.extensions.views.push({
name: "A view",
key: "adapted-view",
template: '<mct-view key="\'' +
viewKey +
'\'" ' +
'mct-object="domainObject">' +
'</mct-view>'
});
this.legacyBundle.extensions.newViews =
this.legacyBundle.extensions.newViews || [];
this.legacyBundle.extensions.newViews.push({
factory: viewFactory,
key: viewKey
});
}
};
MCT.prototype.start = function () {
legacyRegistry.register('adapter', this.legacyBundle);
this.emit('start');
};
MCT.prototype.regions = {
main: "MAIN"
};
MCT.prototype.verbs = {
mutate: function (domainObject, mutator) {
return domainObject.useCapability('mutation', mutator)
.then(function () {
var persistence = domainObject.getCapability('persistence');
return persistence.persist();
});
}
};
return MCT;
});

16
src/adapter/bundle.js Normal file
View File

@ -0,0 +1,16 @@
define([
'legacyRegistry',
'./directives/MCTView'
], function (legacyRegistry, MCTView) {
legacyRegistry.register('src/adapter', {
"extensions": {
"directives": [
{
key: "mctView",
implementation: MCTView,
depends: ["newViews[]"]
}
]
}
});
});

View File

@ -0,0 +1,51 @@
define(['angular'], function (angular) {
function MCTView(newViews) {
var factories = {};
newViews.forEach(function (newView) {
factories[newView.key] = newView.factory;
});
return {
restrict: 'E',
link: function (scope, element, attrs) {
var key = undefined;
var mctObject = undefined;
function maybeShow() {
if (!factories[key]) {
return;
}
if (!mctObject) {
return;
}
var view = factories[key](mctObject);
var elements = view.elements();
element.empty();
element.append(elements);
}
function setKey(k) {
key = k;
maybeShow();
}
function setObject(obj) {
mctObject = obj;
maybeShow();
}
scope.$watch('key', setKey);
scope.$watch('mctObject', setObject);
},
scope: {
key: "=",
mctObject: "="
}
};
}
return MCTView;
});

51
src/api/Type.js Normal file
View File

@ -0,0 +1,51 @@
define(function () {
/**
* @typedef TypeDefinition
* @property {Metadata} metadata displayable metadata about this type
* @property {function (object)} [initialize] a function which initializes
* the model for new domain objects of this type
* @property {boolean} [creatable] true if users should be allowed to
* create this type (default: false)
*/
/**
*
* @param {TypeDefinition} definition
* @constructor
*/
function Type(definition) {
this.definition = definition;
this.views = {};
}
Type.prototype.view = function (region, factory) {
if (arguments.length > 1) {
this.views[region] = factory;
}
return this.views[region];
};
/**
* Get a definition for this type that can be registered using the
* legacy bundle format.
* @private
*/
Type.prototype.toLegacyDefinition = function () {
var def = {};
def.name = this.definition.metadata.label;
def.glyph = this.definition.metadata.glyph;
def.description = this.definition.metadata.description;
if (this.definition.initialize) {
def.model = {};
this.definition.initialize(def.model);
}
if (this.definition.creatable) {
def.features = ['creation'];
}
return def;
};
return Type;
});

21
src/api/View.js Normal file
View File

@ -0,0 +1,21 @@
define(['EventEmitter'], function (EventEmitter) {
function View() {
EventEmitter.call(this);
}
View.prototype = Object.create(EventEmitter.prototype);
['elements', 'model'].forEach(function (method) {
View.prototype[method] = function (value) {
this.viewState =
this.viewState || { elements: [], model: undefined };
if (arguments.length > 0) {
this.viewState[method] = value;
this.emit(method, value);
}
return this.viewState[method];
}
});
return View;
});

12
src/api/api.js Normal file
View File

@ -0,0 +1,12 @@
define([
'./Type',
'./View'
], function (
Type,
View
) {
return {
Type: Type,
View: View
};
});

View File

@ -48,6 +48,7 @@ requirejs.config({
"angular-route": "bower_components/angular-route/angular-route.min",
"csv": "bower_components/comma-separated-values/csv.min",
"es6-promise": "bower_components/es6-promise/promise.min",
"EventEmitter": "bower_components/eventemitter3/index",
"moment": "bower_components/moment/moment",
"moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format",
"saveAs": "bower_components/FileSaver.js/FileSaver.min",
@ -64,6 +65,9 @@ requirejs.config({
"angular-route": {
"deps": [ "angular" ]
},
"EventEmitter": {
"exports": "EventEmitter"
},
"moment-duration-format": {
"deps": [ "moment" ]
},

127
tutorial-server/app.js Normal file
View File

@ -0,0 +1,127 @@
/*global require,process,console*/
var CONFIG = {
port: 8081,
dictionary: "dictionary.json",
interval: 1000
};
(function () {
"use strict";
var WebSocketServer = require('ws').Server,
fs = require('fs'),
wss = new WebSocketServer({ port: CONFIG.port }),
dictionary = JSON.parse(fs.readFileSync(CONFIG.dictionary, "utf8")),
spacecraft = {
"prop.fuel": 77,
"prop.thrusters": "OFF",
"comms.recd": 0,
"comms.sent": 0,
"pwr.temp": 245,
"pwr.c": 8.15,
"pwr.v": 30
},
histories = {},
listeners = [];
function updateSpacecraft() {
spacecraft["prop.fuel"] = Math.max(
0,
spacecraft["prop.fuel"] -
(spacecraft["prop.thrusters"] === "ON" ? 0.5 : 0)
);
spacecraft["pwr.temp"] = spacecraft["pwr.temp"] * 0.985
+ Math.random() * 0.25 + Math.sin(Date.now());
spacecraft["pwr.c"] = spacecraft["pwr.c"] * 0.985;
spacecraft["pwr.v"] = 30 + Math.pow(Math.random(), 3);
}
function generateTelemetry() {
var timestamp = Date.now(), sent = 0;
Object.keys(spacecraft).forEach(function (id) {
var state = { timestamp: timestamp, value: spacecraft[id] };
histories[id] = histories[id] || []; // Initialize
histories[id].push(state);
spacecraft["comms.sent"] += JSON.stringify(state).length;
});
listeners.forEach(function (listener) {
listener();
});
}
function update() {
updateSpacecraft();
generateTelemetry();
}
function handleConnection(ws) {
var subscriptions = {}, // Active subscriptions for this connection
handlers = { // Handlers for specific requests
dictionary: function () {
ws.send(JSON.stringify({
type: "dictionary",
value: dictionary
}));
},
subscribe: function (id) {
subscriptions[id] = true;
},
unsubscribe: function (id) {
delete subscriptions[id];
},
history: function (id) {
ws.send(JSON.stringify({
type: "history",
id: id,
value: histories[id]
}));
}
};
function notifySubscribers() {
Object.keys(subscriptions).forEach(function (id) {
var history = histories[id];
if (history) {
ws.send(JSON.stringify({
type: "data",
id: id,
value: history[history.length - 1]
}));
}
});
}
// Listen for requests
ws.on('message', function (message) {
var parts = message.split(' '),
handler = handlers[parts[0]];
if (handler) {
handler.apply(handlers, parts.slice(1));
}
});
// Stop sending telemetry updates for this connection when closed
ws.on('close', function () {
listeners = listeners.filter(function (listener) {
return listener !== notifySubscribers;
});
});
// Notify subscribers when telemetry is updated
listeners.push(notifySubscribers);
}
update();
setInterval(update, CONFIG.interval);
wss.on('connection', handleConnection);
console.log("Example spacecraft running on port ");
console.log("Press Enter to toggle thruster state.");
process.stdin.on('data', function (data) {
spacecraft['prop.thrusters'] =
(spacecraft['prop.thrusters'] === "OFF") ? "ON" : "OFF";
console.log("Thrusters " + spacecraft["prop.thrusters"]);
});
}());

View File

@ -0,0 +1,66 @@
{
"name": "Example Spacecraft",
"identifier": "sc",
"subsystems": [
{
"name": "Propulsion",
"identifier": "prop",
"measurements": [
{
"name": "Fuel",
"identifier": "prop.fuel",
"units": "kilograms",
"type": "float"
},
{
"name": "Thrusters",
"identifier": "prop.thrusters",
"units": "None",
"type": "string"
}
]
},
{
"name": "Communications",
"identifier": "comms",
"measurements": [
{
"name": "Received",
"identifier": "comms.recd",
"units": "bytes",
"type": "integer"
},
{
"name": "Sent",
"identifier": "comms.sent",
"units": "bytes",
"type": "integer"
}
]
},
{
"name": "Power",
"identifier": "pwr",
"measurements": [
{
"name": "Generator Temperature",
"identifier": "pwr.temp",
"units": "\u0080C",
"type": "float"
},
{
"name": "Generator Current",
"identifier": "pwr.c",
"units": "A",
"type": "float"
},
{
"name": "Generator Voltage",
"identifier": "pwr.v",
"units": "V",
"type": "float"
}
]
}
]
}

View File

@ -0,0 +1,66 @@
define([
'legacyRegistry',
'./src/controllers/BarGraphController'
], function (
legacyRegistry,
BarGraphController
) {
legacyRegistry.register("tutorials/bargraph", {
"name": "Bar Graph",
"description": "Provides the Bar Graph view of telemetry elements.",
"extensions": {
"views": [
{
"name": "Bar Graph",
"key": "example.bargraph",
"glyph": "H",
"templateUrl": "templates/bargraph.html",
"needs": [ "telemetry" ],
"delegation": true,
"editable": true,
"toolbar": {
"sections": [
{
"items": [
{
"name": "Low",
"property": "low",
"required": true,
"control": "textfield",
"size": 4
},
{
"name": "Middle",
"property": "middle",
"required": true,
"control": "textfield",
"size": 4
},
{
"name": "High",
"property": "high",
"required": true,
"control": "textfield",
"size": 4
}
]
}
]
}
}
],
"stylesheets": [
{
"stylesheetUrl": "css/bargraph.css"
}
],
"controllers": [
{
"key": "BarGraphController",
"implementation": BarGraphController,
"depends": [ "$scope", "telemetryHandler" ]
}
]
}
});
});

View File

@ -0,0 +1,35 @@
<div class="example-bargraph" ng-controller="BarGraphController">
<div class="example-tick-labels">
<div ng-repeat="value in [low, middle, high] track by $index"
class="example-tick-label"
style="bottom: {{ toPercent(value) }}%">
{{value}}
</div>
</div>
<div class="example-graph-area">
<div ng-repeat="telemetryObject in telemetryObjects"
style="left: {{barWidth * $index}}%; width: {{barWidth}}%"
class="example-bar-holder">
<div class="example-bar"
ng-style="{
bottom: getBottom(telemetryObject) + '%',
top: getTop(telemetryObject) + '%'
}">
</div>
</div>
<div style="bottom: {{ toPercent(middle) }}%"
class="example-graph-tick">
</div>
</div>
<div class="example-bar-labels">
<div ng-repeat="telemetryObject in telemetryObjects"
style="left: {{barWidth * $index}}%; width: {{barWidth}}%"
class="example-bar-holder example-label">
<mct-representation key="'label'"
mct-object="telemetryObject">
</mct-representation>
</div>
</div>
</div>

View File

@ -0,0 +1,75 @@
define(function () {
function BarGraphController($scope, telemetryHandler) {
var handle;
// Expose configuration constants directly in scope
function exposeConfiguration() {
$scope.low = $scope.configuration.low;
$scope.middle = $scope.configuration.middle;
$scope.high = $scope.configuration.high;
}
// Populate a default value in the configuration
function setDefault(key, value) {
if ($scope.configuration[key] === undefined) {
$scope.configuration[key] = value;
}
}
// Getter-setter for configuration properties (for view proxy)
function getterSetter(property) {
return function (value) {
value = parseFloat(value);
if (!isNaN(value)) {
$scope.configuration[property] = value;
exposeConfiguration();
}
return $scope.configuration[property];
};
}
// Add min/max defaults
setDefault('low', -1);
setDefault('middle', 0);
setDefault('high', 1);
exposeConfiguration($scope.configuration);
// Expose view configuration options
if ($scope.selection) {
$scope.selection.proxy({
low: getterSetter('low'),
middle: getterSetter('middle'),
high: getterSetter('high')
});
}
// Convert value to a percent between 0-100
$scope.toPercent = function (value) {
var pct = 100 * (value - $scope.low) /
($scope.high - $scope.low);
return Math.min(100, Math.max(0, pct));
};
// Get bottom and top (as percentages) for current value
$scope.getBottom = function (telemetryObject) {
var value = handle.getRangeValue(telemetryObject);
return $scope.toPercent(Math.min($scope.middle, value));
};
$scope.getTop = function (telemetryObject) {
var value = handle.getRangeValue(telemetryObject);
return 100 - $scope.toPercent(Math.max($scope.middle, value));
};
// Use the telemetryHandler to get telemetry objects here
handle = telemetryHandler.handle($scope.domainObject, function () {
$scope.telemetryObjects = handle.getTelemetryObjects();
$scope.barWidth =
100 / Math.max(($scope.telemetryObjects).length, 1);
});
// Release subscriptions when scope is destroyed
$scope.$on('$destroy', handle.unsubscribe);
}
return BarGraphController;
});

View File

@ -0,0 +1,90 @@
define([
'legacyRegistry',
'./src/ExampleTelemetryServerAdapter',
'./src/ExampleTelemetryInitializer',
'./src/ExampleTelemetryModelProvider'
], function (
legacyRegistry,
ExampleTelemetryServerAdapter,
ExampleTelemetryInitializer,
ExampleTelemetryModelProvider
) {
legacyRegistry.register("tutorials/telemetry", {
"name": "Example Telemetry Adapter",
"extensions": {
"types": [
{
"name": "Spacecraft",
"key": "example.spacecraft",
"glyph": "o"
},
{
"name": "Subsystem",
"key": "example.subsystem",
"glyph": "o",
"model": { "composition": [] }
},
{
"name": "Measurement",
"key": "example.measurement",
"glyph": "T",
"model": { "telemetry": {} },
"telemetry": {
"source": "example.source",
"domains": [
{
"name": "Time",
"key": "timestamp"
}
]
}
}
],
"roots": [
{
"id": "example:sc",
"priority": "preferred",
"model": {
"type": "example.spacecraft",
"name": "My Spacecraft",
"composition": []
}
}
],
"services": [
{
"key": "example.adapter",
"implementation": "ExampleTelemetryServerAdapter.js",
"depends": [ "$q", "EXAMPLE_WS_URL" ]
}
],
"constants": [
{
"key": "EXAMPLE_WS_URL",
"priority": "fallback",
"value": "ws://localhost:8081"
}
],
"runs": [
{
"implementation": "ExampleTelemetryInitializer.js",
"depends": [ "example.adapter", "objectService" ]
}
],
"components": [
{
"provides": "modelService",
"type": "provider",
"implementation": "ExampleTelemetryModelProvider.js",
"depends": [ "example.adapter", "$q" ]
},
{
"provides": "telemetryService",
"type": "provider",
"implementation": "ExampleTelemetryProvider.js",
"depends": [ "example.adapter", "$q" ]
}
]
}
});
});

View File

@ -0,0 +1,47 @@
define(
function () {
"use strict";
var TAXONOMY_ID = "example:sc",
PREFIX = "example_tlm:";
function ExampleTelemetryInitializer(adapter, objectService) {
// Generate a domain object identifier for a dictionary element
function makeId(element) {
return PREFIX + element.identifier;
}
// When the dictionary is available, add all subsystems
// to the composition of My Spacecraft
function initializeTaxonomy(dictionary) {
// Get the top-level container for dictionary objects
// from a group of domain objects.
function getTaxonomyObject(domainObjects) {
return domainObjects[TAXONOMY_ID];
}
// Populate
function populateModel(taxonomyObject) {
return taxonomyObject.useCapability(
"mutation",
function (model) {
model.name =
dictionary.name;
model.composition =
dictionary.subsystems.map(makeId);
}
);
}
// Look up My Spacecraft, and populate it accordingly.
objectService.getObjects([TAXONOMY_ID])
.then(getTaxonomyObject)
.then(populateModel);
}
adapter.dictionary().then(initializeTaxonomy);
}
return ExampleTelemetryInitializer;
}
);

View File

@ -0,0 +1,78 @@
define(
function () {
"use strict";
var PREFIX = "example_tlm:",
FORMAT_MAPPINGS = {
float: "number",
integer: "number",
string: "string"
};
function ExampleTelemetryModelProvider(adapter, $q) {
var modelPromise, empty = $q.when({});
// Check if this model is in our dictionary (by prefix)
function isRelevant(id) {
return id.indexOf(PREFIX) === 0;
}
// Build a domain object identifier by adding a prefix
function makeId(element) {
return PREFIX + element.identifier;
}
// Create domain object models from this dictionary
function buildTaxonomy(dictionary) {
var models = {};
// Create & store a domain object model for a measurement
function addMeasurement(measurement) {
var format = FORMAT_MAPPINGS[measurement.type];
models[makeId(measurement)] = {
type: "example.measurement",
name: measurement.name,
telemetry: {
key: measurement.identifier,
ranges: [{
key: "value",
name: "Value",
units: measurement.units,
format: format
}]
}
};
}
// Create & store a domain object model for a subsystem
function addSubsystem(subsystem) {
var measurements =
(subsystem.measurements || []);
models[makeId(subsystem)] = {
type: "example.subsystem",
name: subsystem.name,
composition: measurements.map(makeId)
};
measurements.forEach(addMeasurement);
}
(dictionary.subsystems || []).forEach(addSubsystem);
return models;
}
// Begin generating models once the dictionary is available
modelPromise = adapter.dictionary().then(buildTaxonomy);
return {
getModels: function (ids) {
// Return models for the dictionary only when they
// are relevant to the request.
return ids.some(isRelevant) ? modelPromise : empty;
}
};
}
return ExampleTelemetryModelProvider;
}
);

View File

@ -0,0 +1,80 @@
define(
['./ExampleTelemetrySeries'],
function (ExampleTelemetrySeries) {
"use strict";
var SOURCE = "example.source";
function ExampleTelemetryProvider(adapter, $q) {
var subscribers = {};
// Used to filter out requests for telemetry
// from some other source
function matchesSource(request) {
return (request.source === SOURCE);
}
// Listen for data, notify subscribers
adapter.listen(function (message) {
var packaged = {};
packaged[SOURCE] = {};
packaged[SOURCE][message.id] =
new ExampleTelemetrySeries([message.value]);
(subscribers[message.id] || []).forEach(function (cb) {
cb(packaged);
});
});
return {
requestTelemetry: function (requests) {
var packaged = {},
relevantReqs = requests.filter(matchesSource);
// Package historical telemetry that has been received
function addToPackage(history) {
packaged[SOURCE][history.id] =
new ExampleTelemetrySeries(history.value);
}
// Retrieve telemetry for a specific measurement
function handleRequest(request) {
var key = request.key;
return adapter.history(key).then(addToPackage);
}
packaged[SOURCE] = {};
return $q.all(relevantReqs.map(handleRequest))
.then(function () { return packaged; });
},
subscribe: function (callback, requests) {
var keys = requests.filter(matchesSource)
.map(function (req) { return req.key; });
function notCallback(cb) {
return cb !== callback;
}
function unsubscribe(key) {
subscribers[key] =
(subscribers[key] || []).filter(notCallback);
if (subscribers[key].length < 1) {
adapter.unsubscribe(key);
}
}
keys.forEach(function (key) {
subscribers[key] = subscribers[key] || [];
adapter.subscribe(key);
subscribers[key].push(callback);
});
return function () {
keys.forEach(unsubscribe);
};
}
};
}
return ExampleTelemetryProvider;
}
);

View File

@ -0,0 +1,23 @@
/*global define*/
define(
function () {
"use strict";
function ExampleTelemetrySeries(data) {
return {
getPointCount: function () {
return data.length;
},
getDomainValue: function (index) {
return (data[index] || {}).timestamp;
},
getRangeValue: function (index) {
return (data[index] || {}).value;
}
};
}
return ExampleTelemetrySeries;
}
);

View File

@ -0,0 +1,60 @@
define(
[],
function () {
"use strict";
function ExampleTelemetryServerAdapter($q, wsUrl) {
var ws = new WebSocket(wsUrl),
histories = {},
listeners = [],
dictionary = $q.defer();
// Handle an incoming message from the server
ws.onmessage = function (event) {
var message = JSON.parse(event.data);
switch (message.type) {
case "dictionary":
dictionary.resolve(message.value);
break;
case "history":
histories[message.id].resolve(message);
delete histories[message.id];
break;
case "data":
listeners.forEach(function (listener) {
listener(message);
});
break;
}
};
// Request dictionary once connection is established
ws.onopen = function () {
ws.send("dictionary");
};
return {
dictionary: function () {
return dictionary.promise;
},
history: function (id) {
histories[id] = histories[id] || $q.defer();
ws.send("history " + id);
return histories[id].promise;
},
subscribe: function (id) {
ws.send("subscribe " + id);
},
unsubscribe: function (id) {
ws.send("unsubscribe " + id);
},
listen: function (callback) {
listeners.push(callback);
}
};
}
return ExampleTelemetryServerAdapter;
}
);

19
tutorials/todo/bundle.js Normal file
View File

@ -0,0 +1,19 @@
define([
'legacyRegistry',
'./src/controllers/TodoController'
], function (
legacyRegistry,
TodoController
) {
legacyRegistry.register("tutorials/todo", {
"name": "To-do Plugin",
"description": "Allows creating and editing to-do lists.",
"extensions": {
"stylesheets": [
{
"stylesheetUrl": "css/todo.css"
}
]
}
});
});

View File

@ -0,0 +1,29 @@
<div ng-controller="TodoController" class="example-todo">
<div class="example-button-group">
<a ng-class="{ selected: checkVisibility(true) }"
ng-click="setVisibility(true)">All</a>
<a ng-class="{ selected: checkVisibility(false, false) }"
ng-click="setVisibility(false, false)">Incomplete</a>
<a ng-class="{ selected: checkVisibility(false, true) }"
ng-click="setVisibility(false, true)">Complete</a>
</div>
<ul>
<li ng-repeat="task in model.tasks"
ng-class="{ 'example-task-completed': task.completed }"
ng-if="showTask(task)">
<input type="checkbox"
ng-checked="task.completed"
ng-click="toggleCompletion($index)">
<span ng-click="selectTask($index)"
ng-class="{ selected: isSelected($index) }"
class="example-task-description">
{{task.description}}
</span>
</li>
</ul>
<div ng-if="model.tasks.length < 1" class="example-message">
There are no tasks to show.
</div>
</div>

View File

@ -0,0 +1,97 @@
define(function () {
// Form to display when adding new tasks
var NEW_TASK_FORM = {
name: "Add a Task",
sections: [{
rows: [{
name: 'Description',
key: 'description',
control: 'textfield',
required: true
}]
}]
};
function TodoController($scope, dialogService) {
var showAll = true,
showCompleted;
// Persist changes made to a domain object's model
function persist() {
var persistence =
$scope.domainObject.getCapability('persistence');
return persistence && persistence.persist();
}
// Remove a task
function removeTaskAtIndex(taskIndex) {
$scope.domainObject.useCapability('mutation', function (model) {
model.tasks.splice(taskIndex, 1);
});
persist();
}
// Add a task
function addNewTask(task) {
$scope.domainObject.useCapability('mutation', function (model) {
model.tasks.push(task);
});
persist();
}
// Change which tasks are visible
$scope.setVisibility = function (all, completed) {
showAll = all;
showCompleted = completed;
};
// Check if current visibility settings match
$scope.checkVisibility = function (all, completed) {
return showAll ? all : (completed === showCompleted);
};
// Toggle the completion state of a task
$scope.toggleCompletion = function (taskIndex) {
$scope.domainObject.useCapability('mutation', function (model) {
var task = model.tasks[taskIndex];
task.completed = !task.completed;
});
persist();
};
// Check whether a task should be visible
$scope.showTask = function (task) {
return showAll || (showCompleted === !!(task.completed));
};
// Handle selection state in edit mode
if ($scope.selection) {
// Expose the ability to select tasks
$scope.selectTask = function (taskIndex) {
$scope.selection.select({
removeTask: function () {
removeTaskAtIndex(taskIndex);
$scope.selection.deselect();
},
taskIndex: taskIndex
});
};
// Expose a check for current selection state
$scope.isSelected = function (taskIndex) {
return ($scope.selection.get() || {}).taskIndex ===
taskIndex;
};
// Expose a view-level selection proxy
$scope.selection.proxy({
addTask: function () {
dialogService.getUserInput(NEW_TASK_FORM, {})
.then(addNewTask);
}
});
}
}
return TodoController;
});

View File

@ -0,0 +1,5 @@
<li>
<input type="checkbox" class="example-task-checked">
<span class="example-task-description">
</span>
</li>

14
tutorials/todo/todo.html Normal file
View File

@ -0,0 +1,14 @@
<div class="example-todo">
<div class="example-button-group">
<a class="example-todo-button-all">All</a>
<a class="example-todo-button-incomplete">Incomplete</a>
<a class="example-todo-button-complete">Complete</a>
</div>
<ul class="example-todo-task-list">
</ul>
<div class="example-message">
There are no tasks to show.
</div>
</div>

108
tutorials/todo/todo.js Normal file
View File

@ -0,0 +1,108 @@
define([
"text!./todo.html",
"text!./todo-task.html",
"zepto"
], function (todoTemplate, taskTemplate, $) {
/**
* @param {mct.MCT} mct
*/
return function todoPlugin(mct) {
var todoType = new mct.Type({
metadata: {
label: "To-Do List",
glyph: "2",
description: "A list of things that need to be done."
},
initialize: function (model) {
model.tasks = [
{ description: "This is a task." }
];
},
creatable: true
});
function TodoView(domainObject) {
mct.View.apply(this);
this.filterValue = "all";
this.elements($(todoTemplate));
var $els = $(this.elements());
this.$buttons = {
all: $els.find('.example-todo-button-all'),
incomplete: $els.find('.example-todo-button-incomplete'),
complete: $els.find('.example-todo-button-complete')
};
this.initialize();
this.on('model', this.render.bind(this));
this.model(domainObject);
}
TodoView.prototype = Object.create(mct.View.prototype);
TodoView.prototype.setFilter = function (value) {
this.filterValue = value;
this.render();
};
TodoView.prototype.initialize = function () {
Object.keys(this.$buttons).forEach(function (k) {
this.$buttons[k].on('click', this.setFilter.bind(this, k));
}, this);
};
TodoView.prototype.render = function () {
var $els = $(this.elements());
var domainObject = this.model();
var tasks = domainObject.getModel().tasks;
var $message = $els.find('.example-message');
var $list = $els.find('.example-todo-task-list');
var $buttons = this.$buttons;
var filters = {
all: function () {
return true;
},
incomplete: function (task) {
return !task.completed;
},
complete: function (task) {
return task.completed;
}
};
var filterValue = this.filterValue;
Object.keys($buttons).forEach(function (k) {
$buttons[k].toggleClass('selected', filterValue === k);
});
tasks = tasks.filter(filters[filterValue]);
$list.empty();
tasks.forEach(function (task, index) {
var $taskEls = $(taskTemplate);
var $checkbox = $taskEls.find('.example-task-checked');
$checkbox.prop('checked', task.completed);
$taskEls.find('.example-task-description')
.text(task.description);
$checkbox.on('change', function () {
var checked = !!$checkbox.prop('checked');
mct.verbs.mutate(domainObject, function (model) {
model.tasks[index].completed = checked;
});
});
$list.append($taskEls);
});
$message.toggle(tasks.length < 1);
};
todoType.view(mct.regions.main, function (domainObject) {
return new TodoView(domainObject);
});
mct.type('example.todo', todoType);
return mct;
};
});