tutorial consistency . (#1079)

* [API] use new-style objects consistently

* rewrite todo tutorial in test-api.html

* [API] Add API doc, update object API

* [Tutorials] Rename tutorials, remove old

* Fix Links

* updates

* initial

* hope this works

* Object Utils always return new objects instead of mutating existing objects

* keep domain object model in-sync when listening

Keep the domain object model in sync with the latest version when
listening for mutation events.

* Remove old-style plugins

* Update views to use new API

* Tidy Code

* Update API Docs

* Add Plugin API and Example
This commit is contained in:
Pete Richards 2016-07-21 14:39:02 -07:00 committed by GitHub
parent 18843cee48
commit 7890fcae69
17 changed files with 599 additions and 154 deletions

91
API.md Normal file
View File

@ -0,0 +1,91 @@
# Open MCT API
The Open MCT framework public api can be utilized by building the application (`gulp install`) and then copying the file from `dist/main.js` to your directory
of choice.
Open MCT supports AMD, CommonJS, and standard browser loading; it's easy to use
in your project.
## Overview
Open MCT's goal is to allow you to browse, create, edit, and visualize all of the domain knowledge you need on a daily basis.
To do this, the main building block provided by Open MCT is the domain object-- the temperature sensor on the starboard solar panel, an overlay plot comparing the results of all temperature sensor, the command dictionary for a spacecraft, the individual commands in that dictionary, your "my documents" folder: all of these things are domain objects.
Domain objects have Types-- so a specific instrument temperature sensor is a "Telemetry Point," and turning on a drill for a certain duration of time is an "Activity". Types allow you to form an ontology of knowledge and provide an abstraction for grouping, visualizing, and interpreting data.
And then we have Views. Views allow you to visualize a domain object. Views can apply to specific domain objects; they may also apply to certain types of domain objects, or they may apply to everything. Views are simply a method of visualizing domain objects.
Regions allow you to specify what views are displayed for specific types of domain objects in response to different user actions-- for instance, you may want to display a different view while editing, or you may want to update the toolbar display when objects are selected. Regions allow you to map views to specific user actions.
Domain objects can be mutated and persisted, developers can create custom actions and apply them to domain objects, and many more things can be done. For more information, read on.
## The API
### `MCT.Type(options)`
Status: First Draft
Returns a `typeInstance`. `options` is an object supporting the following properties:
* `metadata`: `object` defining metadata used in displaying the object; has the following properties:
* `label`: `string`, the human-readible name of the type. used in menus and inspector.
* `glyph`: `string`, the name of the icon to display for this type, used in labels.
* `description`: `string`, a human readible description of the object and what it is for.
* `initialize`: `function` which initializes new instances of this type. it is called with an object, should add any default properties to that object.
* `creatable`: `boolean`, if true, this object will be visible in the create menu.
### `MCT.type(typeKey, typeInstance)`
Status: First Draft
Register a `typeInstance` with a given Type `key` (a `string`). There can only be one `typeInstance` registered per type `key`. typeInstances must be registered before they can be utilized.
### `MCT.Objects`
Status: First Draft
Allows you to register object providers, which allows you to integrate domain objects from various different sources. Also implements methods for mutation and persistence of objects. See [Object API](src/api/objects/README.md) for more details.
### `MCT.Composition`
Status: First Draft
Objects can contain other objects, and the Composition API allows you to fetch the composition of any given domain object, or implement custom methods for defining composition as necessary.
### `MCT.view(region, definition)`
Status: First Draft
Register a view factory for a specific region. View factories receive an instance of a domain object and return a `View` for that object, or return undefined if they do not know how to generate a view for that object.
* `ViewDefinition`: an object with the following properties:
* `canView(domainObject)`: should return truthy if the view is valid for a given domain object, falsy if it is not capable of generating a view for that object.
* `view(domainObject)`: should instantate and return a `View` for the given object.
* `metadata()`: a function that returns metadata about this view. Optional.
* `View`: an object containing a number of lifecycle methods:
* `view.show(container)`: instantiate a view (a set of dom elements) and attach it to the container.
* `view.destroy(container)`: remove any listeners and expect your dom elements to be destroyed.
For a basic introduction to views & types, check out these tutorials:
* [custom-view](custom-view.html) -- Implementing a custom view with vanilla javascript.
* [custom-view-react](custom-view-react.html) -- Implementing a custom view with React.
### `MCT.conductor`
Status: First Draft
The time conductor is an API that facilitates time synchronization across multiple components. Components that would like to be "time aware" may attach listeners to the time conductor API to allow them to remain synchronized with other components. For more information ont he time conductor API, please look at the API draft here: https://github.com/nasa/openmct/blob/66220b89ca568075f107505ba414de9457dc0427/platform/features/conductor-redux/src/README.md
### `MCT.systems`
Status: Not Implemented, Needs to be ported from old system.
A registry for different time system definitions. Based upon the previous time format system which utilized the "formats" extension category.
### `MCT.run([container])`
Status: Stable Draft
Run the MCT application, loading the application into the `container`, a DOM element. If a container is not specified, the application is injected into the body of the page.
### `MCT.install(plugin)`
Status: Stable Draft
Install a plugin in MCT. Must be called before calling `run`. Plugins are functions which are invoked with the `MCT` instance as their first argument, and are expected to use the MCT public API to add functionality.
For an example of writing a plugin, check out [plugin-example.html](plugin-example.html)

144
custom-view-react.html Normal file
View File

@ -0,0 +1,144 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Implementing a Custom Type and View </title>
<script src="dist/main.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.2.1/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.2.1/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.min.js"></script>
</head>
<body>
<script type="text/babel">
// First, we're going to create the Todo List type, so that users can create
// todo lists.
MCT.type('example.todo', new MCT.Type({
metadata: {
label: "To-Do List",
glyph: "2",
description: "A list of things that need to be done."
},
initialize: function (object) {
object.tasks = [
{ description: "This is a task." }
];
},
creatable: true
}));
/*
Refresh the page, and you should be able to create new Todo Lists.
unfortunately, when you navigate to a Todo list, you see a blank page. let's
fix that by adding a main view for that todo list.
If you're wondering why this is commented out, well, it's because we'll
write a new version later.
*/
var Task = React.createClass({
render: function() {
return (
<li>
<input type="checkbox"
checked={this.props.checked}/>
<span>{this.props.description}</span>
</li>
);
}
});
var TaskList = React.createClass({
render: function () {
var taskNodes = this.props.tasks.map(function(task) {
return (
<Task checked={task.checked}
description={task.description}/>
);
});
return (
<ul>
{taskNodes}
</ul>
);
}
});
MCT.view(MCT.regions.main, {
canView: function (domainObject) {
return domainObject.type === 'example.todo';
},
view: function (domainObject) {
var mutableObject = MCT.Objects.getMutable(domainObject);
return {
show: function (container) {
ReactDOM.render(
<TaskList tasks={domainObject.tasks}/>,
container
);
mutableObject.on('tasks', function (tasks) {
ReactDOM.render(
<TaskList tasks={tasks}/>,
container
);
});
}
};
}
});
/*
Refresh the page and you should see a todo list with checkboxes! Now let's
Allow you to add tasks by mutating the object. We'll add a toolbar view to
do this.
*/
var TaskToolbar = React.createClass({
render: function () {
return (
<button onClick={this.props.addTask}>Add Task</button>
);
}
});
MCT.view(MCT.regions.toolbar, {
canView: function (domainObject) {
return domainObject.type === 'example.todo';
},
view: function (domainObject) {
var mutableObject = MCT.Objects.getMutable(domainObject);
function addTask(event) {
var description = prompt('Task description');
var tasks = mutableObject.get('tasks');
tasks.push({
description: description,
complete: false
});
mutableObject.set('tasks', tasks);
}
return {
show: function (container) {
ReactDOM.render(
<TaskToolbar addTask={addTask}/>,
container
);
},
}
}
});
/*
Refresh the page, edit the todo list, and you'll have a button that allows
you to add tasks! Unfortunately, new tasks don't show in the list. Why?
Well, if your view should update on mutation, you need to set up the correct
listeners. Let's update the TodoView we made earlier:
*/
MCT.run();
</script>
</body>
</html>

160
custom-view.html Normal file
View File

@ -0,0 +1,160 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Implementing a Custom Type and View </title>
<script src="dist/main.js"></script>
</head>
<body>
<script>
// First, we're going to create the Todo List type, so that users can create
// todo lists.
MCT.type('example.todo', new MCT.Type({
metadata: {
label: "To-Do List",
glyph: "2",
description: "A list of things that need to be done."
},
initialize: function (object) {
object.tasks = [
{ description: "This is a task." }
];
},
creatable: true
}));
/*
Refresh the page, and you should be able to create new Todo Lists.
unfortunately, when you navigate to a Todo list, you see a blank page. let's
fix that by adding a main view for that todo list.
If you're wondering why this is commented out, well, it's because we'll
write a new version later.
*/
// MCT.view(MCT.regions.main, {
// canView: function (domainObject) {
// return domainObject.type === 'example.todo';
// },
// view: function (domainObject) {
// function renderTask(task) {
// return [
// '<li>',
// '<input type="checkbox"' + (task.complete ? ' checked="true"' : '') + '>',
// '<span>' + task.description + '</span>',
// '</li>'
// ].join('');
// };
//
// function renderTaskList() {
// return [
// '<ul>',
// domainObject.tasks.map(renderTask).join(''),
// '</ul>'
// ].join('');
// };
//
// return {
// show: function (container) {
// container.innerHTML = renderTaskList();
// }
// };
// }
// });
/*
Refresh the page and you should see a todo list with checkboxes! Now let's
Allow you to add tasks by mutating the object. We'll add a toolbar view to
do this.
*/
MCT.view(MCT.regions.toolbar, {
canView: function (domainObject) {
return domainObject.type === 'example.todo';
},
view: function (domainObject) {
var mutableObject = MCT.Objects.getMutable(domainObject);
function addTask(event) {
var description = prompt('Task description');
var tasks = mutableObject.get('tasks');
tasks.push({
description: description,
complete: false
});
mutableObject.set('tasks', tasks);
}
return {
show: function (container) {
container.addEventListener('click', addTask);
container.innerHTML = '<button>Add Task</button>';
},
destroy: function (container) {
container.removeEventListener('click', addTask);
}
}
}
});
/*
Refresh the page, edit the todo list, and you'll have a button that allows
you to add tasks! Unfortunately, new tasks don't show in the list. Why?
Well, if your view should update on mutation, you need to set up the correct
listeners. Let's update the TodoView we made earlier:
*/
MCT.view(MCT.regions.main, {
canView: function(domainObject) {
return domainObject.type === 'example.todo'
},
view: function (domainObject) {
var mutableObject = MCT.Objects.getMutable(domainObject);
function renderTask(task) {
return [
'<li>',
'<input type="checkbox"' + (task.complete ? ' checked="true"' : '') + '>',
'<span>' + task.description + '</span>',
'</li>'
].join('');
}
function renderTaskList(tasks) {
return [
'<ul>',
tasks.map(renderTask).join(''),
'</ul>'
].join('');
}
function onCheckboxChange(event) {
var checkbox = event.target;
var taskEl = checkbox.parentNode;
var taskList = taskEl.parentNode;
var taskIndex = [].slice.apply(taskList.children).indexOf(taskEl);
mutableObject.set('tasks[' + taskIndex + '].complete', checkbox.checked);
}
return {
show: function (container) {
container.innerHTML = renderTaskList(domainObject.tasks);
mutableObject.on('tasks', function (tasks) {
container.innerHTML = renderTaskList(tasks);
});
container.addEventListener('change', onCheckboxChange);
},
destroy: function () {
mutableObject.stopListening();
}
};
}
});
MCT.run();
</script>
</body>
</html>

View File

@ -31,21 +31,16 @@
<script type="text/javascript">
require(['main'], function (mct) {
require([
'./tutorials/grootprovider/groots',
'./tutorials/todo/todo',
'./example/imagery/bundle',
'./example/eventGenerator/bundle',
'./example/generator/bundle',
], function (grootify, todoPlugin) {
grootify(mct);
todoPlugin(mct);
], function () {
mct.run();
})
});
</script>
<link rel="stylesheet" href="platform/commonUI/general/res/css/startup-base.css">
<link rel="stylesheet" href="platform/commonUI/general/res/css/openmct.css">
<link rel="stylesheet" href="tutorials/todo/todo.css">
<link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/png" href="platform/commonUI/general/res/images/favicons/favicon-16x16.png" sizes="16x16">

64
object-provider.html Normal file
View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Groot Tutorial</title>
<script src="dist/main.js"></script>
</head>
<body>
<script>
// First, we need a source of objects, so we're going to define a few
// objects that we wish to expose. The first object is a folder which
// contains the other objects.
var GROOT_ROOT = {
name: 'I am groot',
type: 'folder',
composition: [
{
namespace: 'groot',
identifier: 'arms'
},
{
namespace: 'groot',
identifier: 'legs'
},
{
namespace: 'groot',
identifier: 'torso'
}
]
};
// Now, we will define an object provider. This will allow us to fetch the
// GROOT_ROOT object, plus any other objects in the `groot` namespace.
// For more info, see the Object API documentation.
MCT.Objects.addProvider('groot', {
// we'll only define a get function, objects from this provider will
// not be mutable.
get: function (key) {
if (key.identifier === 'groot') {
return Promise.resolve(GROOT_ROOT);
}
return Promise.resolve({
name: 'Groot\'s ' + key.identifier
});
}
});
// Finally, we need to add a "ROOT." This is an identifier for an object
// that Open MCT will load at run time and show at the top-level of the
// navigation tree.
MCT.Objects.addRoot({
namespace: 'groot',
identifier: 'groot'
});
MCT.run();
</script>
</body>
</html>

View File

@ -167,7 +167,10 @@ define(
* @memberof platform/core.MutationCapability#
*/
MutationCapability.prototype.listen = function (listener) {
return this.specificMutationTopic.listen(listener);
return this.specificMutationTopic.listen(function (newModel) {
this.domainObject.model = JSON.parse(JSON.stringify(newModel));
listener(newModel);
}.bind(this));
};
/**

50
plugin-example.html Normal file
View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Implementing a basic plugin</title>
<script src="dist/main.js"></script>
</head>
<body>
<script>
function WebPlugin(websites) {
this.websites = websites;
var ROOTS = websites.reduce(function (rootMap, website) {
rootMap[website] = {
namespace: website,
identifier: 'page'
};
return rootMap;
}, {});
function installPlugin(MCT) {
Object.keys(ROOTS).forEach(function (rootUrl) {
MCT.Objects.addRoot(ROOTS[rootUrl]);
MCT.Objects.addProvider(rootUrl, {
get: function () {
return Promise.resolve({
type: 'example.page',
url: rootUrl,
name: rootUrl
});
}
});
});
}
return installPlugin;
}
var myWebPlugin = WebPlugin([
'http://www.wikipedia.org/',
'http://nasa.github.io/openmct'
]);
MCT.install(myWebPlugin);
MCT.run();
</script>
</body>
</html>

View File

@ -6,7 +6,8 @@ define([
'text!./adapter/templates/edit-object-replacement.html',
'./ui/Dialog',
'./Selection',
'./api/objects/bundle'
'./api/objects/bundle',
'./api/objects/object-utils'
], function (
EventEmitter,
legacyRegistry,
@ -14,7 +15,9 @@ define([
api,
editObjectTemplate,
Dialog,
Selection
Selection,
objectAPIBundle,
objectUtils
) {
function MCT() {
EventEmitter.call(this);
@ -67,9 +70,11 @@ define([
this.legacyExtension('policies', {
category: "view",
implementation: function Policy() {
this.allow = function (v, domainObject) {
if (v.key === adaptedViewKey) {
return definition.canView(domainObject);
this.allow = function (view, domainObject) {
if (view.key === adaptedViewKey) {
var model = domainObject.getModel();
var newDO = objectUtils.toNewFormat(model);
return definition.canView(newDO);
}
return true;
};
@ -81,6 +86,13 @@ define([
region: region,
key: viewKey
});
this.legacyExtension('services', {
key: 'PublicAPI',
implementation: function () {
return this;
}.bind(this)
});
};
MCT.prototype.type = function (key, type) {
@ -114,6 +126,16 @@ define([
this.emit('start');
};
/**
* Install a plugin in MCT.
*
* @param `Function` plugin -- a plugin install function which will be
* invoked with the mct instance.
*/
MCT.prototype.install = function (plugin) {
plugin(this);
};
MCT.prototype.regions = {
main: "MAIN",
toolbar: "TOOLBAR"

View File

@ -8,7 +8,10 @@ define([
{
key: "mctView",
implementation: MCTView,
depends: ["newViews[]"]
depends: [
"newViews[]",
"PublicAPI"
]
}
]
}

View File

@ -1,8 +1,13 @@
define([
'angular',
'./Region'
], function (angular, Region) {
function MCTView(newViews) {
'./Region',
'../../api/objects/object-utils'
], function (
angular,
Region,
objectUtils
) {
function MCTView(newViews, PublicAPI) {
var definitions = {};
newViews.forEach(function (newView) {
@ -29,8 +34,12 @@ define([
}
function setObject(obj) {
mctObject = obj;
maybeShow();
mctObject = undefined;
PublicAPI.Objects.get(objectUtils.parseKeyString(obj.getId()))
.then(function (mobj) {
mctObject = mobj;
maybeShow();
});
}
function setRegionId(r) {

View File

@ -24,7 +24,7 @@ define(function () {
*/
Type.prototype.check = function (domainObject) {
// Depends on assignment from MCT.
return domainObject.getModel().type === this.key;
return domainObject.type === this.key;
};
/**

View File

@ -30,7 +30,7 @@ define([
var keyString = utils.makeKeyString(key);
return this.objectService.getObjects([keyString])
.then(function (results) {
var model = JSON.parse(JSON.stringify(results[keyString].getModel()));
var model = results[keyString].getModel();
return utils.toNewFormat(model, key);
});
};

View File

@ -13,15 +13,6 @@ define([
/**
Object API. Intercepts the existing object API while also exposing
A new Object API.
MCT.objects.get('mine')
.then(function (root) {
console.log(root);
MCT.objects.getComposition(root)
.then(function (composition) {
console.log(composition)
})
});
*/
var Objects = {},

View File

@ -16,85 +16,39 @@ The above key would encode into the identifier, `elastic:myIdentifier`.
When interacting with the API you will be dealing with key objects.
## Roots
"Roots" are objects that Open MCT will load at run time and form the basic entry point for users. You can register new root objects by calling the
# Configuring the Object API
The following methods should be used before calling run. They allow you to
configure the persistence space of MCT.
* `MCT.objects.addRoot(key)` -- add a "ROOT" to Open MCT by specifying it's
key.
* `MCT.objects.addRoot(key)` -- add a "ROOT" to Open MCT by specifying it's key.
* `MCT.objects.removeRoot(key)` -- Remove a "ROOT" from Open MCT by key.
* `MCT.objects.addProvider(namespace, provider)` -- register an object provider
for a specific namespace. See below for documentation on the provider
interface.
* `MCT.objects.addProvider(namespace, provider)` -- register an object provider for a specific namespace. See below for documentation on the provider interface.
# defining providers
# Using the object API
The object API provides methods for getting, saving, and deleting objects.
The object API provides methods for getting, saving, and deleting objects. Plugin developers may not frequently need to interact with this API, as they should receive instances of the objects as needed.
* MCT.objects.get(key) -> returns promise for an object
* MCT.objects.save(object) -> returns promise that is resolved when object
has been saved
* MCT.objects.delete(object) -> returns promise that is resolved when object has
been deleted
* MCT.objects.save(object) -> returns promise that is resolved when object has been saved
* MCT.objects.delete(object) -> returns promise that is resolved when object has been deleted
## Configuration Example: Adding a groot
The following example adds a new root object for groot and populates it with
some pieces of groot.
```javascript
var ROOT_KEY = {
namespace: 'groot',
identifier: 'groot'
};
var GROOT_ROOT = {
name: 'I am groot',
type: 'folder',
composition: [
{
namespace: 'groot',
identifier: 'arms'
},
{
namespace: 'groot',
identifier: 'legs'
},
{
namespace: 'groot',
identifier: 'torso'
}
]
};
var GrootProvider = {
get: function (key) {
if (key.identifier === 'groot') {
return Promise.resolve(GROOT_ROOT);
}
return Promise.resolve({
name: 'Groot\'s ' + key.identifier
});
}
};
mct.Objects.addRoot(ROOT_KEY);
mct.Objects.addProvider('groot', GrootProvider);
```
See the tutorial in [object-provider.html](object-provider.html).
### Making a custom provider:
All methods on the provider interface are optional, so you do not need
to modify them.
All methods on the provider interface are optional.
* `provider.get(key)` -> promise for a domain object.
* `provider.save(domainObject)` -> returns promise that is fulfilled when object
has been saved.
* `provider.delete(domainObject)` -> returns promise that is fulfilled when
object has been deleted.
* `provider.get(key)` -> returns promise for a domain object.
* `provider.save(domainObject)` -> returns promise that is fulfilled when object has been saved.
* `provider.delete(domainObject)` -> returns promise that is fulfilled when object has been deleted.

View File

@ -12,13 +12,27 @@ define([
}
var namespace = '',
identifier = key;
for (var i = 0, escaped = false, len=key.length; i < len; i++) {
if (key[i] === ":" && !escaped) {
namespace = key.slice(0, i);
identifier = key.slice(i + 1);
break;
for (var i = 0, escaped = false, len=key.length; i < key.length; i++) {
if (escaped) {
escaped = false;
} else {
if (key[i] === "\\") {
escaped = true;
continue;
}
if (key[i] === ":") {
// namespace = key.slice(0, i);
identifier = key.slice(i + 1);
break;
}
}
namespace += key[i];
}
if (key === namespace) {
namespace = '';
}
return {
namespace: namespace,
identifier: identifier
@ -42,6 +56,7 @@ define([
// Converts composition to use key strings instead of keys
var toOldFormat = function (model) {
model = JSON.parse(JSON.stringify(model));
delete model.key;
if (model.composition) {
model.composition = model.composition.map(makeKeyString);
@ -51,6 +66,7 @@ define([
// converts composition to use keys instead of key strings
var toNewFormat = function (model, key) {
model = JSON.parse(JSON.stringify(model));
model.key = key;
if (model.composition) {
model.composition = model.composition.map(parseKeyString);

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Minimal Test</title>
<script src="dist/main.js"></script>
</head>
<body>
<script>
MCT.run();
</script>
</body>
</html>

View File

@ -1,44 +0,0 @@
define(function () {
return function grootPlugin(mct) {
var ROOT_KEY = {
namespace: 'groot',
identifier: 'groot'
};
var GROOT_ROOT = {
name: 'I am groot',
type: 'folder',
composition: [
{
namespace: 'groot',
identifier: 'arms'
},
{
namespace: 'groot',
identifier: 'legs'
},
{
namespace: 'groot',
identifier: 'torso'
}
]
};
var GrootProvider = {
get: function (key) {
if (key.identifier === 'groot') {
return Promise.resolve(GROOT_ROOT);
}
return Promise.resolve({
name: 'Groot\'s ' + key.identifier
});
}
};
mct.Objects.addRoot(ROOT_KEY);
mct.Objects.addProvider('groot', GrootProvider);
return mct;
};
});