mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 10:44:21 +00:00
Compare commits
82 Commits
edit-sizin
...
plot-sync
Author | SHA1 | Date | |
---|---|---|---|
57abac96b2 | |||
204f6be9d0 | |||
c988c6c43b | |||
c143475f3e | |||
b248f57daa | |||
f4e8c5ecf6 | |||
11ef7c0ccd | |||
9c125e0454 | |||
439a654f6d | |||
9d32a3f1c3 | |||
705e25df8a | |||
787d6887dc | |||
1d516240a8 | |||
90ee0a309f | |||
72d05283da | |||
dc91a94f0e | |||
0243aa6584 | |||
e5d869f01e | |||
d4e3e6689c | |||
0363d0e8ad | |||
3669e776a9 | |||
5d3adc6a7f | |||
c1b2db848a | |||
5d19294c11 | |||
9b8d5f3f9c | |||
d03f323a9b | |||
54a453e5a0 | |||
14894cf197 | |||
0c6786198a | |||
6d077b775d | |||
144437a06e | |||
557cd91b21 | |||
39d3e92094 | |||
7529a86d01 | |||
d34e36831c | |||
aa8fa9168a | |||
3f1b7e0a87 | |||
5ec3b98d1c | |||
1ad5094b72 | |||
b54ee2257e | |||
fcef4274e5 | |||
744a5340d3 | |||
d140051054 | |||
8da74f2665 | |||
2390278b97 | |||
8a66731271 | |||
0a9ea48355 | |||
01d93306f3 | |||
0588f9190a | |||
1378b57567 | |||
9e12886c66 | |||
2d352ac574 | |||
284dec4903 | |||
5a0656c700 | |||
425655bae0 | |||
50b4d5cb28 | |||
bc62d7d5ae | |||
c0dcf4495e | |||
a51b9bc63f | |||
ff003c3dab | |||
de7c4d2ce3 | |||
4b07930305 | |||
5a49ac16b1 | |||
91b150c064 | |||
9506d309b0 | |||
c9bd60f50e | |||
cf15ff5c07 | |||
6bbdfcdfbe | |||
06e93ff520 | |||
550e7a15e6 | |||
71c54cd541 | |||
e81b8e53dc | |||
84e6928f54 | |||
ba688fe62c | |||
c533e10352 | |||
04f47b3db6 | |||
717fa5edf4 | |||
14f5f048fb | |||
72929500d3 | |||
471adde923 | |||
6c5d5f3d00 | |||
2262fef29b |
@ -21,5 +21,6 @@
|
||||
"shadow": "outer",
|
||||
"strict": "implied",
|
||||
"undef": true,
|
||||
"unused": "vars"
|
||||
"unused": "vars",
|
||||
"latedef": "nofunc"
|
||||
}
|
||||
|
15
API.md
15
API.md
@ -879,6 +879,21 @@ openmct.install(openmct.plugins.CouchDB('http://localhost:9200'))
|
||||
* `openmct.plugins.Espresso` and `openmct.plugins.Snow` are two different
|
||||
themes (dark and light) available for Open MCT. Note that at least one
|
||||
of these themes must be installed for Open MCT to appear correctly.
|
||||
* `openmct.plugins.URLIndicatorPlugin` adds an indicator which shows the
|
||||
availability of a URL with the following options:
|
||||
- `url` : URL to indicate the status of
|
||||
- `cssClass`: Icon to show in the status bar, defaults to `icon-database`, [list of all icons](https://nasa.github.io/openmct/style-guide/#/browse/styleguide:home?view=items)
|
||||
- `interval`: Interval between checking the connection, defaults to `10000`
|
||||
- `label` Name showing up as text in the status bar, defaults to url
|
||||
```javascript
|
||||
openmct.install(openmct.plugins.URLIndicatorPlugin({
|
||||
url: 'http://google.com',
|
||||
cssClass: 'check',
|
||||
interval: 10000,
|
||||
label: 'Google'
|
||||
})
|
||||
);
|
||||
```
|
||||
* `openmct.plugins.LocalStorage` provides persistence of user-created
|
||||
objects in browser-local storage. This is particularly useful in
|
||||
development environments.
|
||||
|
@ -88,7 +88,7 @@ and [`gulp`](http://gulpjs.com/).
|
||||
|
||||
To build Open MCT for deployment:
|
||||
|
||||
`npm run prepublish`
|
||||
`npm run prepare`
|
||||
|
||||
This will compile and minify JavaScript sources, as well as copy over assets.
|
||||
The contents of the `dist` folder will contain a runnable Open MCT
|
||||
|
@ -17,7 +17,7 @@
|
||||
"screenfull": "^3.0.0",
|
||||
"node-uuid": "^1.4.7",
|
||||
"comma-separated-values": "^3.6.4",
|
||||
"FileSaver.js": "^0.0.2",
|
||||
"file-saver": "^1.3.3",
|
||||
"zepto": "^1.1.6",
|
||||
"eventemitter3": "^1.2.0",
|
||||
"lodash": "3.10.1",
|
||||
|
10
circle.yml
10
circle.yml
@ -1,3 +1,11 @@
|
||||
machine:
|
||||
node:
|
||||
version: 4.7.0
|
||||
|
||||
dependencies:
|
||||
pre:
|
||||
- npm install -g npm@latest
|
||||
|
||||
deployment:
|
||||
production:
|
||||
branch: master
|
||||
@ -16,4 +24,4 @@ test:
|
||||
general:
|
||||
branches:
|
||||
ignore:
|
||||
- gh-pages
|
||||
- gh-pages
|
@ -2283,7 +2283,7 @@ To install build dependencies (only needs to be run once):
|
||||
|
||||
To build:
|
||||
|
||||
`npm run prepublish`
|
||||
`npm run prepare`
|
||||
|
||||
This will compile and minify JavaScript sources, as well as copy over assets.
|
||||
The contents of the `dist` folder will contain a runnable Open MCT
|
||||
|
121
docs/src/guide/security.md
Normal file
121
docs/src/guide/security.md
Normal file
@ -0,0 +1,121 @@
|
||||
# Security Guide
|
||||
|
||||
Open MCT is a rich client with plugin support that executes as a single page
|
||||
web application in a browser environment. Security concerns and
|
||||
vulnerabilities associated with the web as a platform should be considered
|
||||
before deploying Open MCT (or any other web application) for mission or
|
||||
production usage.
|
||||
|
||||
This document describes several important points to consider when developing
|
||||
for or deploying Open MCT securely. Other resources such as
|
||||
[Open Web Application Security Project (OWASP)](https://www.owasp.org)
|
||||
provide a deeper and more general overview of security for web applications.
|
||||
|
||||
|
||||
## Security Model
|
||||
|
||||
Open MCT has been architected assuming the following deployment pattern:
|
||||
|
||||
* A tagged, tested Open MCT version will be used.
|
||||
* Externally authored plugins will be installed.
|
||||
* A server will provide persistent storage, telemetry, and other shared data.
|
||||
* Authorization, authentication, and auditing will be handled by a server.
|
||||
|
||||
|
||||
## Security Procedures
|
||||
|
||||
The Open MCT team secures our code base using a combination of code review,
|
||||
dependency review, and periodic security reviews. Static analysis performed
|
||||
during automated verification additionally safeguards against common
|
||||
coding errors which may result in vulnerabilities.
|
||||
|
||||
|
||||
### Code Review
|
||||
|
||||
All contributions are reviewed by internal team members. External
|
||||
contributors receive increased scrutiny for security and quality,
|
||||
and must sign a licensing agreement.
|
||||
|
||||
### Dependency Review
|
||||
|
||||
Before integrating third-party dependencies, they are reviewed for security
|
||||
and quality, with consideration given to authors and users of these
|
||||
dependencies, as well as review of open source code.
|
||||
|
||||
### Periodic Security Reviews
|
||||
|
||||
Open MCT's code, design, and architecture are periodically reviewed
|
||||
(approximately annually) for common security issues, such as the
|
||||
[OWASP Top Ten](https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project).
|
||||
|
||||
|
||||
## Security Concerns
|
||||
|
||||
Certain security concerns deserve special attention when deploying Open MCT,
|
||||
or when authoring plugins.
|
||||
|
||||
### Identity Spoofing
|
||||
|
||||
Open MCT issues calls to web services with the privileges of a logged in user.
|
||||
Compromised sources (either for Open MCT itself or a plugin) could
|
||||
therefore allow malicious code to execute with those privileges.
|
||||
|
||||
To avoid this:
|
||||
|
||||
* Serve Open MCT and other scripts over SSL (https rather than http)
|
||||
to prevent man-in-the-middle attacks.
|
||||
* Exercise precautions such as security reviews for any plugins or
|
||||
applications built for or with Open MCT to reject malicious changes.
|
||||
|
||||
### Information Disclosure
|
||||
|
||||
If Open MCT is used to handle or display sensitive data, any components
|
||||
(such as adapter plugins) must take care to avoid leaking or disclosing
|
||||
this information. For example, avoid sending sensitive data to third-party
|
||||
servers or insecure APIs.
|
||||
|
||||
### Data Tampering
|
||||
|
||||
The web application architecture leaves open the possibility that direct
|
||||
calls will be made to back-end services, circumventing Open MCT entirely.
|
||||
As such, Open MCT assumes that server components will perform any necessary
|
||||
data validation during calls issues to the server.
|
||||
|
||||
Additionally, plugins which serialize and write data to the server must
|
||||
escape that data to avoid database injection attacks, and similar.
|
||||
|
||||
### Repudiation
|
||||
|
||||
Open MCT assumes that servers log any relevant interactions and associates
|
||||
these with a user identity; the specific user actions taken within the
|
||||
application are assumed not to be of concern for auditing.
|
||||
|
||||
In the absence of server-side logging, users may disclaim (maliciously,
|
||||
mistakenly, or otherwise) actions taken within the system without any
|
||||
way to prove otherwise.
|
||||
|
||||
If keeping client-level interactions is important, this will need to be
|
||||
implemented via a plugin.
|
||||
|
||||
### Denial-of-service
|
||||
|
||||
Open MCT assumes that server-side components will be insulated against
|
||||
denial-of-service attacks. Services should only permit resource-intensive
|
||||
tasks to be initiated by known or trusted users.
|
||||
|
||||
### Elevation of Privilege
|
||||
|
||||
Corollary to the assumption that servers guide against identity spoofing,
|
||||
Open MCT assumes that services do not allow a user to act with
|
||||
inappropriately escalated privileges. Open MCT cannot protect against
|
||||
such escalation; in the clearest case, a malicious actor could interact
|
||||
with web services directly to exploit such a vulnerability.
|
||||
|
||||
## Additional Reading
|
||||
|
||||
The following resources have been used as a basis for identifying potential
|
||||
security threats to Open MCT deployments in preparation of this document:
|
||||
|
||||
* [STRIDE model](https://www.owasp.org/index.php/Threat_Risk_Modeling#STRIDE)
|
||||
* [Attack Surface Analysis Cheat Sheet](https://www.owasp.org/index.php/Attack_Surface_Analysis_Cheat_Sheet)
|
||||
* [XSS Prevention Cheat Sheet](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet)
|
@ -30,7 +30,8 @@ define([
|
||||
amplitude: 1,
|
||||
period: 10,
|
||||
offset: 0,
|
||||
dataRateInHz: 1
|
||||
dataRateInHz: 1,
|
||||
phase: 0
|
||||
};
|
||||
|
||||
function GeneratorProvider() {
|
||||
@ -50,16 +51,19 @@ define([
|
||||
'amplitude',
|
||||
'period',
|
||||
'offset',
|
||||
'dataRateInHz'
|
||||
'dataRateInHz',
|
||||
'phase',
|
||||
];
|
||||
|
||||
request = request || {};
|
||||
|
||||
var workerRequest = {};
|
||||
|
||||
props.forEach(function (prop) {
|
||||
if (domainObject.telemetry && domainObject.telemetry.hasOwnProperty(prop)) {
|
||||
workerRequest[prop] = domainObject.telemetry[prop];
|
||||
}
|
||||
if (request.hasOwnProperty(prop)) {
|
||||
if (request && request.hasOwnProperty(prop)) {
|
||||
workerRequest[prop] = request[prop];
|
||||
}
|
||||
if (!workerRequest[prop]) {
|
||||
@ -67,7 +71,7 @@ define([
|
||||
}
|
||||
workerRequest[prop] = Number(workerRequest[prop]);
|
||||
});
|
||||
|
||||
workerRequest.name = domainObject.name;
|
||||
return workerRequest;
|
||||
};
|
||||
|
||||
|
80
example/generator/StateGeneratorProvider.js
Normal file
80
example/generator/StateGeneratorProvider.js
Normal file
@ -0,0 +1,80 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2017, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
|
||||
], function (
|
||||
|
||||
) {
|
||||
|
||||
function StateGeneratorProvider() {
|
||||
|
||||
}
|
||||
|
||||
function pointForTimestamp(timestamp, duration, name) {
|
||||
return {
|
||||
name: name,
|
||||
utc: Math.floor(timestamp / duration) * duration,
|
||||
value: Math.floor(timestamp / duration) % 2
|
||||
};
|
||||
}
|
||||
|
||||
StateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
|
||||
return domainObject.type === 'example.state-generator';
|
||||
};
|
||||
|
||||
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
|
||||
var duration = domainObject.telemetry.duration * 1000;
|
||||
|
||||
var interval = setInterval(function () {
|
||||
var now = Date.now();
|
||||
callback(pointForTimestamp(now, duration, domainObject.name));
|
||||
}, duration);
|
||||
|
||||
return function () {
|
||||
clearInterval(interval);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
StateGeneratorProvider.prototype.supportsRequest = function (domainObject, options) {
|
||||
return domainObject.type === 'example.state-generator';
|
||||
};
|
||||
|
||||
StateGeneratorProvider.prototype.request = function (domainObject, options) {
|
||||
var start = options.start;
|
||||
var end = options.end;
|
||||
var duration = domainObject.telemetry.duration * 1000;
|
||||
if (options.strategy === 'latest' || options.size === 1) {
|
||||
start = end;
|
||||
}
|
||||
var data = [];
|
||||
while (start <= end && data.length < 5000) {
|
||||
data.push(pointForTimestamp(start, duration, domainObject.name));
|
||||
start += 5000;
|
||||
}
|
||||
return Promise.resolve(data);
|
||||
};
|
||||
|
||||
return StateGeneratorProvider;
|
||||
|
||||
});
|
@ -44,9 +44,7 @@ define([
|
||||
message = message.data;
|
||||
var callback = this.callbacks[message.id];
|
||||
if (callback) {
|
||||
if (callback(message)) {
|
||||
delete this.callbacks[message.id];
|
||||
}
|
||||
callback(message);
|
||||
}
|
||||
};
|
||||
|
||||
@ -72,6 +70,7 @@ define([
|
||||
deferred.resolve = resolve;
|
||||
deferred.reject = reject;
|
||||
});
|
||||
var messageId;
|
||||
|
||||
function callback(message) {
|
||||
if (message.error) {
|
||||
@ -79,33 +78,27 @@ define([
|
||||
} else {
|
||||
deferred.resolve(message.data);
|
||||
}
|
||||
return true;
|
||||
delete this.callbacks[messageId];
|
||||
}
|
||||
|
||||
this.dispatch('request', request, callback);
|
||||
messageId = this.dispatch('request', request, callback.bind(this));
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
WorkerInterface.prototype.subscribe = function (request, cb) {
|
||||
var isCancelled = false;
|
||||
|
||||
var callback = function (message) {
|
||||
if (isCancelled) {
|
||||
return true;
|
||||
}
|
||||
function callback(message) {
|
||||
cb(message.data);
|
||||
};
|
||||
|
||||
var messageId = this.dispatch('subscribe', request, callback)
|
||||
var messageId = this.dispatch('subscribe', request, callback);
|
||||
|
||||
return function () {
|
||||
isCancelled = true;
|
||||
this.dispatch('unsubscribe', {
|
||||
id: messageId
|
||||
});
|
||||
delete this.callbacks[messageId];
|
||||
}.bind(this);
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
@ -62,10 +62,11 @@
|
||||
self.postMessage({
|
||||
id: message.id,
|
||||
data: {
|
||||
name: data.name,
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60*60*24*1000,
|
||||
sin: sin(nextStep, data.period, data.amplitude, data.offset),
|
||||
cos: cos(nextStep, data.period, data.amplitude, data.offset)
|
||||
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase),
|
||||
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase)
|
||||
}
|
||||
});
|
||||
nextStep += step;
|
||||
@ -82,21 +83,22 @@
|
||||
}
|
||||
|
||||
function onRequest(message) {
|
||||
var data = message.data;
|
||||
if (data.end == undefined) {
|
||||
data.end = Date.now();
|
||||
var request = message.data;
|
||||
if (request.end == undefined) {
|
||||
request.end = Date.now();
|
||||
}
|
||||
if (data.start == undefined){
|
||||
data.start = data.end - FIFTEEN_MINUTES;
|
||||
if (request.start == undefined){
|
||||
request.start = request.end - FIFTEEN_MINUTES;
|
||||
}
|
||||
|
||||
var now = Date.now();
|
||||
var start = data.start;
|
||||
var end = data.end > now ? now : data.end;
|
||||
var amplitude = data.amplitude;
|
||||
var period = data.period;
|
||||
var offset = data.offset;
|
||||
var dataRateInHz = data.dataRateInHz;
|
||||
var start = request.start;
|
||||
var end = request.end > now ? now : request.end;
|
||||
var amplitude = request.amplitude;
|
||||
var period = request.period;
|
||||
var offset = request.offset;
|
||||
var dataRateInHz = request.dataRateInHz;
|
||||
var phase = request.phase;
|
||||
|
||||
var step = 1000 / dataRateInHz;
|
||||
var nextStep = start - (start % step) + step;
|
||||
@ -105,10 +107,11 @@
|
||||
|
||||
for (; nextStep < end && data.length < 5000; nextStep += step) {
|
||||
data.push({
|
||||
name: request.name,
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60*60*24*1000,
|
||||
sin: sin(nextStep, period, amplitude, offset),
|
||||
cos: cos(nextStep, period, amplitude, offset)
|
||||
sin: sin(nextStep, period, amplitude, offset, phase),
|
||||
cos: cos(nextStep, period, amplitude, offset, phase)
|
||||
});
|
||||
}
|
||||
self.postMessage({
|
||||
@ -117,14 +120,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
function cos(timestamp, period, amplitude, offset) {
|
||||
function cos(timestamp, period, amplitude, offset, phase) {
|
||||
return amplitude *
|
||||
Math.cos(timestamp / period / 1000 * Math.PI * 2) + offset;
|
||||
Math.cos(phase + (timestamp / period / 1000 * Math.PI * 2)) + offset;
|
||||
}
|
||||
|
||||
function sin(timestamp, period, amplitude, offset) {
|
||||
function sin(timestamp, period, amplitude, offset, phase) {
|
||||
return amplitude *
|
||||
Math.sin(timestamp / period / 1000 * Math.PI * 2) + offset;
|
||||
Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + offset;
|
||||
}
|
||||
|
||||
function sendError(error, message) {
|
||||
|
@ -23,10 +23,12 @@
|
||||
|
||||
define([
|
||||
"./GeneratorProvider",
|
||||
"./SinewaveLimitCapability"
|
||||
"./SinewaveLimitCapability",
|
||||
"./StateGeneratorProvider"
|
||||
], function (
|
||||
GeneratorProvider,
|
||||
SinewaveLimitCapability
|
||||
SinewaveLimitCapability,
|
||||
StateGeneratorProvider
|
||||
) {
|
||||
|
||||
var legacyExtensions = {
|
||||
@ -46,6 +48,75 @@ define([
|
||||
openmct.legacyExtension(type, extension)
|
||||
})
|
||||
});
|
||||
|
||||
openmct.types.addType("example.state-generator", {
|
||||
name: "State Generator",
|
||||
description: "For development use. Generates test enumerated telemetry by cycling through a given set of states",
|
||||
cssClass: "icon-telemetry",
|
||||
creatable: true,
|
||||
form: [
|
||||
{
|
||||
name: "State Duration (seconds)",
|
||||
control: "textfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "duration",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"duration"
|
||||
],
|
||||
pattern: "^\\d*(\\.\\d*)?$"
|
||||
}
|
||||
],
|
||||
initialize: function (object) {
|
||||
object.telemetry = {
|
||||
duration: 5,
|
||||
values: [
|
||||
{
|
||||
key: "name",
|
||||
name: "Name"
|
||||
},
|
||||
{
|
||||
key: "utc",
|
||||
name: "Time",
|
||||
format: "utc",
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "state",
|
||||
source: "value",
|
||||
name: "State",
|
||||
format: "enum",
|
||||
enumerations: [
|
||||
{
|
||||
value: 0,
|
||||
string: "OFF"
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
string: "ON"
|
||||
}
|
||||
],
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "value",
|
||||
name: "Value",
|
||||
hints: {
|
||||
range: 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
openmct.telemetry.addProvider(new StateGeneratorProvider());
|
||||
|
||||
openmct.types.addType("generator", {
|
||||
name: "Sine Wave Generator",
|
||||
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
|
||||
@ -99,6 +170,18 @@ define([
|
||||
"dataRateInHz"
|
||||
],
|
||||
pattern: "^\\d*(\\.\\d*)?$"
|
||||
},
|
||||
{
|
||||
name: "Phase (radians)",
|
||||
control: "textfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "phase",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"phase"
|
||||
],
|
||||
pattern: "^\\d*(\\.\\d*)?$"
|
||||
}
|
||||
],
|
||||
initialize: function (object) {
|
||||
@ -107,7 +190,12 @@ define([
|
||||
amplitude: 1,
|
||||
offset: 0,
|
||||
dataRateInHz: 1,
|
||||
phase: 0,
|
||||
values: [
|
||||
{
|
||||
key: "name",
|
||||
name: "Name"
|
||||
},
|
||||
{
|
||||
key: "utc",
|
||||
name: "Time",
|
||||
@ -142,6 +230,7 @@ define([
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
openmct.telemetry.addProvider(new GeneratorProvider());
|
||||
};
|
||||
|
||||
|
@ -48,8 +48,9 @@ define([
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg"
|
||||
];
|
||||
|
||||
function pointForTimestamp(timestamp) {
|
||||
function pointForTimestamp(timestamp, name) {
|
||||
return {
|
||||
name: name,
|
||||
utc: Math.floor(timestamp / 5000) * 5000,
|
||||
url: IMAGE_SAMPLES[Math.floor(timestamp / 5000) % IMAGE_SAMPLES.length]
|
||||
};
|
||||
@ -61,7 +62,7 @@ define([
|
||||
},
|
||||
subscribe: function (domainObject, callback) {
|
||||
var interval = setInterval(function () {
|
||||
callback(pointForTimestamp(Date.now()));
|
||||
callback(pointForTimestamp(Date.now(), domainObject.name));
|
||||
}, 5000);
|
||||
|
||||
return function (interval) {
|
||||
@ -79,8 +80,8 @@ define([
|
||||
var start = options.start;
|
||||
var end = options.end;
|
||||
var data = [];
|
||||
while (start < end && data.length < 5000) {
|
||||
data.push(pointForTimestamp(start));
|
||||
while (start <= end && data.length < 5000) {
|
||||
data.push(pointForTimestamp(start, domainObject.name));
|
||||
start += 5000;
|
||||
}
|
||||
return Promise.resolve(data);
|
||||
@ -93,7 +94,7 @@ define([
|
||||
options.strategy === 'latest';
|
||||
},
|
||||
request: function (domainObject, options) {
|
||||
return Promise.resolve([pointForTimestamp(Date.now())]);
|
||||
return Promise.resolve([pointForTimestamp(Date.now(), domainObject.name)]);
|
||||
}
|
||||
};
|
||||
|
||||
@ -109,6 +110,10 @@ define([
|
||||
initialize: function (object) {
|
||||
object.telemetry = {
|
||||
values: [
|
||||
{
|
||||
name: 'Name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
name: 'Time',
|
||||
key: 'utc',
|
||||
|
@ -58,11 +58,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.w-mct-example {
|
||||
div {
|
||||
margin-bottom: $interiorMarginLg;
|
||||
}
|
||||
}
|
||||
.w-mct-example > div { margin-bottom: $interiorMarginLg; }
|
||||
|
||||
code,
|
||||
pre {
|
||||
|
@ -121,7 +121,7 @@
|
||||
<h2>Palettes</h2>
|
||||
<div class="cols cols1-1">
|
||||
<div class="col">
|
||||
<p>Use a palette to provide color choices. Similar to context menus and dropdowns, palettes should be dismissed when a choice is made within them, or if the user clicks outside one.</p>
|
||||
<p>Use a palette to provide color choices. Similar to context menus and dropdowns, palettes should be dismissed when a choice is made within them, or if the user clicks outside one. Selected palette choices should utilize the <code>selected</code> CSS class to visualize indicate that state.</p>
|
||||
<p>Note that while this example uses static markup for illustrative purposes, don't do this - use a front-end framework with repeaters to build the color choices.</p>
|
||||
</div>
|
||||
<mct-example><div style="height: 220px" title="Ignore me, I'm just here to provide space for this example.">
|
||||
@ -129,9 +129,9 @@
|
||||
<div class="s-button s-menu-button menu-element t-color-palette icon-paint-bucket" ng-controller="ClickAwayController as toggle">
|
||||
<span class="l-click-area" ng-click="toggle.toggle()"></span>
|
||||
<span class="color-swatch" style="background: rgb(255, 0, 0);"></span>
|
||||
<div class="menu l-color-palette" ng-show="toggle.isActive()">
|
||||
<div class="menu l-palette l-color-palette" ng-show="toggle.isActive()">
|
||||
<div class="l-palette-row l-option-row">
|
||||
<div class="l-palette-item s-palette-item " ng-click="ngModel[field] = 'transparent'"></div>
|
||||
<div class="l-palette-item s-palette-item no-selection"></div>
|
||||
<span class="l-palette-item-label">None</span>
|
||||
</div>
|
||||
<div class="l-palette-row">
|
||||
@ -147,7 +147,7 @@
|
||||
<div class="l-palette-item s-palette-item" style="background: rgb(255, 255, 255);"></div>
|
||||
</div>
|
||||
<div class="l-palette-row">
|
||||
<div class="l-palette-item s-palette-item" style="background: rgb(136, 32, 32);"></div>
|
||||
<div class="l-palette-item s-palette-item selected" style="background: rgb(255, 0, 0);"></div>
|
||||
<div class="l-palette-item s-palette-item" style="background: rgb(224, 64, 64);"></div>
|
||||
<div class="l-palette-item s-palette-item" style="background: rgb(240, 160, 72);"></div>
|
||||
<div class="l-palette-item s-palette-item" style="background: rgb(255, 248, 96);"></div>
|
||||
|
@ -22,6 +22,8 @@
|
||||
|
||||
/*global require,__dirname*/
|
||||
|
||||
require("v8-compile-cache");
|
||||
|
||||
var gulp = require('gulp'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
path = require('path'),
|
||||
@ -177,4 +179,4 @@ gulp.task('install', [ 'assets', 'scripts' ]);
|
||||
|
||||
gulp.task('verify', [ 'lint', 'test', 'checkstyle' ]);
|
||||
|
||||
gulp.task('build', [ 'verify', 'install' ]);
|
||||
gulp.task('build', [ 'verify', 'install' ]);
|
@ -25,8 +25,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<title></title>
|
||||
<script src="bower_components/requirejs/require.js">
|
||||
</script>
|
||||
<script src="bower_components/requirejs/require.js"> </script>
|
||||
<script>
|
||||
var THIRTY_MINUTES = 30 * 60 * 1000;
|
||||
|
||||
@ -44,13 +43,16 @@
|
||||
openmct.install(openmct.plugins.ExampleImagery());
|
||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
||||
openmct.install(openmct.plugins.ImportExport());
|
||||
openmct.install(openmct.plugins.AutoflowView({
|
||||
type: "telemetry.panel"
|
||||
}));
|
||||
openmct.install(openmct.plugins.Conductor({
|
||||
menuOptions: [
|
||||
{
|
||||
name: "Fixed",
|
||||
timeSystem: 'utc',
|
||||
bounds: {
|
||||
start: Date.now() - 30 * 60 * 1000,
|
||||
start: Date.now() - THIRTY_MINUTES,
|
||||
end: Date.now()
|
||||
}
|
||||
},
|
||||
@ -65,6 +67,7 @@
|
||||
}
|
||||
]
|
||||
}));
|
||||
openmct.install(openmct.plugins.SummaryWidget());
|
||||
openmct.time.clock('local', {start: -THIRTY_MINUTES, end: 0});
|
||||
openmct.time.timeSystem('utc');
|
||||
openmct.start();
|
||||
|
@ -36,6 +36,7 @@ module.exports = function(config) {
|
||||
files: [
|
||||
{pattern: 'bower_components/**/*.js', included: false},
|
||||
{pattern: 'node_modules/d3-*/**/*.js', included: false},
|
||||
{pattern: 'node_modules/vue/**/*.js', included: false},
|
||||
{pattern: 'src/**/*.js', included: false},
|
||||
{pattern: 'example/**/*.html', included: false},
|
||||
{pattern: 'example/**/*.js', included: false},
|
||||
@ -88,7 +89,8 @@ module.exports = function(config) {
|
||||
"dist/reports/coverage",
|
||||
check: {
|
||||
global: {
|
||||
lines: 80
|
||||
lines: 80,
|
||||
excludes: ['src/plugins/plot/**/*.js']
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -33,13 +33,14 @@ requirejs.config({
|
||||
"moment": "bower_components/moment/moment",
|
||||
"moment-duration-format": "bower_components/moment-duration-format/lib/moment-duration-format",
|
||||
"moment-timezone": "bower_components/moment-timezone/builds/moment-timezone-with-data",
|
||||
"saveAs": "bower_components/FileSaver.js/FileSaver.min",
|
||||
"saveAs": "bower_components/file-saver/FileSaver.min",
|
||||
"screenfull": "bower_components/screenfull/dist/screenfull.min",
|
||||
"text": "bower_components/text/text",
|
||||
"uuid": "bower_components/node-uuid/uuid",
|
||||
"vue": "node_modules/vue/dist/vue.min",
|
||||
"zepto": "bower_components/zepto/zepto.min",
|
||||
"lodash": "bower_components/lodash/lodash",
|
||||
"d3-selection": "node_modules/d3-selection/build/d3-selection.min",
|
||||
"d3-selection": "node_modules/d3-selection/dist/d3-selection.min",
|
||||
"d3-scale": "node_modules/d3-scale/build/d3-scale.min",
|
||||
"d3-axis": "node_modules/d3-axis/build/d3-axis.min",
|
||||
"d3-array": "node_modules/d3-array/build/d3-array.min",
|
||||
@ -66,6 +67,9 @@ requirejs.config({
|
||||
"moment-duration-format": {
|
||||
"deps": ["moment"]
|
||||
},
|
||||
"saveAs": {
|
||||
"exports": "saveAs"
|
||||
},
|
||||
"screenfull": {
|
||||
"exports": "screenfull"
|
||||
},
|
||||
@ -97,6 +101,7 @@ define([
|
||||
var openmct = new MCT();
|
||||
|
||||
openmct.legacyRegistry = defaultRegistry;
|
||||
openmct.install(openmct.plugins.Plot());
|
||||
|
||||
if (typeof BUILD_CONSTANTS !== 'undefined') {
|
||||
openmct.install(buildInfo(BUILD_CONSTANTS));
|
||||
|
@ -15,7 +15,8 @@
|
||||
"d3-time-format": "^2.0.3",
|
||||
"express": "^4.13.1",
|
||||
"minimist": "^1.1.1",
|
||||
"request": "^2.69.0"
|
||||
"request": "^2.69.0",
|
||||
"vue": "^2.5.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bower": "^1.7.7",
|
||||
@ -49,7 +50,8 @@
|
||||
"moment": "^2.11.1",
|
||||
"node-bourbon": "^4.2.3",
|
||||
"requirejs": "2.1.x",
|
||||
"split": "^1.0.0"
|
||||
"split": "^1.0.0",
|
||||
"v8-compile-cache": "^1.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
@ -59,7 +61,7 @@
|
||||
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
|
||||
"otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'",
|
||||
"docs": "npm run jsdoc ; npm run otherdoc",
|
||||
"prepublish": "node ./node_modules/bower/bin/bower install && node ./node_modules/gulp/bin/gulp.js install"
|
||||
"prepare": "node ./node_modules/bower/bin/bower install && node ./node_modules/gulp/bin/gulp.js install"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -57,7 +57,12 @@
|
||||
</div>
|
||||
<mct-representation key="representation.selected.key"
|
||||
mct-object="representation.selected.key && domainObject"
|
||||
class="abs flex-elem grows object-holder-main scroll">
|
||||
class="abs flex-elem grows object-holder-main scroll"
|
||||
mct-selectable="{
|
||||
item: domainObject.useCapability('adapter'),
|
||||
oldItem: domainObject
|
||||
}"
|
||||
mct-init-select>
|
||||
</mct-representation>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -19,12 +19,21 @@
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div ng-controller="InspectorController">
|
||||
<div ng-repeat="region in regions">
|
||||
<div ng-controller="InspectorController as controller">
|
||||
<mct-representation
|
||||
key="region.content.key"
|
||||
mct-object="domainObject"
|
||||
key="'object-properties'"
|
||||
mct-object="controller.selectedItem()"
|
||||
ng-model="ngModel">
|
||||
</mct-representation>
|
||||
</div>
|
||||
|
||||
<div ng-if="!controller.hasProviderView()">
|
||||
<mct-representation
|
||||
key="inspectorKey"
|
||||
mct-object="controller.selectedItem()"
|
||||
ng-model="ngModel">
|
||||
</mct-representation>
|
||||
</div>
|
||||
|
||||
<div class='inspector-provider-view'>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,8 +38,6 @@
|
||||
ng-class="{ last:($index + 1) === contextualParents.length }">
|
||||
<mct-representation key="'label'"
|
||||
mct-object="parent"
|
||||
ng-model="ngModel"
|
||||
ng-click="ngModel.selectedObject = parent"
|
||||
class="location-item">
|
||||
</mct-representation>
|
||||
</span>
|
||||
@ -51,8 +49,6 @@
|
||||
ng-class="{ last:($index + 1) === primaryParents.length }">
|
||||
<mct-representation key="'label'"
|
||||
mct-object="parent"
|
||||
ng-model="ngModel"
|
||||
ng-click="ngModel.selectedObject = parent"
|
||||
class="location-item">
|
||||
</mct-representation>
|
||||
</span>
|
||||
|
@ -20,7 +20,7 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div class="abs top-bar">
|
||||
<div class="title">{{ngModel.title}}</div>
|
||||
<div class="dialog-title">{{ngModel.title}}</div>
|
||||
<div class="hint">All fields marked <span class="req icon-asterisk"></span> are required.</div>
|
||||
</div>
|
||||
<div class='abs editor'>
|
||||
|
@ -1,11 +1,10 @@
|
||||
<div class="l-message"
|
||||
ng-class="'message-severity-' + ngModel.severity">
|
||||
<div class="ui-symbol type-icon message-type"></div>
|
||||
<div class="message-contents">
|
||||
<div class="w-message-contents">
|
||||
<div class="top-bar">
|
||||
<div class="title">{{ngModel.title}}</div>
|
||||
<div class="hint" ng-hide="ngModel.hint === undefined">{{ngModel.hint}}</div>
|
||||
</div>
|
||||
<div class="hint" ng-hide="ngModel.hint === undefined">{{ngModel.hint}}</div>
|
||||
<div class="message-body">
|
||||
<div class="message-action">
|
||||
{{ngModel.actionText}}
|
||||
@ -25,8 +24,6 @@
|
||||
ng-click="ngModel.primaryOption.callback()">
|
||||
{{ngModel.primaryOption.label}}
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,17 +1,17 @@
|
||||
<mct-container key="overlay" class="t-message-list">
|
||||
<div class="message-contents">
|
||||
<div class="abs top-bar">
|
||||
<div class="title">{{ngModel.dialog.title}}</div>
|
||||
<mct-container key="overlay">
|
||||
<div class="t-message-list">
|
||||
<div class="top-bar">
|
||||
<div class="dialog-title">{{ngModel.dialog.title}}</div>
|
||||
<div class="hint">Displaying {{ngModel.dialog.messages.length}} message<span ng-show="ngModel.dialog.messages.length > 1 ||
|
||||
ngModel.dialog.messages.length == 0">s</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="abs message-body">
|
||||
<div class="w-messages">
|
||||
<mct-include
|
||||
ng-repeat="msg in ngModel.dialog.messages | orderBy: '-'"
|
||||
key="'message'" ng-model="msg.model"></mct-include>
|
||||
ng-repeat="msg in ngModel.dialog.messages | orderBy: '-'"
|
||||
key="'message'" ng-model="msg.model"></mct-include>
|
||||
</div>
|
||||
<div class="abs bottom-bar">
|
||||
<div class="bottom-bar">
|
||||
<a ng-repeat="dialogAction in ngModel.dialog.actions"
|
||||
class="s-button major"
|
||||
ng-click="dialogAction.action()">
|
||||
|
@ -21,7 +21,7 @@
|
||||
-->
|
||||
<mct-container key="overlay">
|
||||
<div class="abs top-bar">
|
||||
<div class="title">{{ngModel.dialog.title}}</div>
|
||||
<div class="dialog-title">{{ngModel.dialog.title}}</div>
|
||||
<div class="hint">{{ngModel.dialog.hint}}</div>
|
||||
</div>
|
||||
<div class='abs editor'>
|
||||
|
@ -121,7 +121,8 @@ define([
|
||||
"key": "ElementsController",
|
||||
"implementation": ElementsController,
|
||||
"depends": [
|
||||
"$scope"
|
||||
"$scope",
|
||||
"openmct"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -299,9 +300,6 @@ define([
|
||||
{
|
||||
"key": "edit-elements",
|
||||
"template": elementsTemplate,
|
||||
"uses": [
|
||||
"composition"
|
||||
],
|
||||
"gestures": [
|
||||
"drop"
|
||||
]
|
||||
@ -385,7 +383,10 @@ define([
|
||||
]
|
||||
},
|
||||
{
|
||||
"implementation": EditToolbarRepresenter
|
||||
"implementation": EditToolbarRepresenter,
|
||||
"depends": [
|
||||
"openmct"
|
||||
]
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
|
@ -61,7 +61,12 @@
|
||||
<mct-representation key="representation.selected.key"
|
||||
mct-object="representation.selected.key && domainObject"
|
||||
class="abs flex-elem grows object-holder-main scroll"
|
||||
toolbar="toolbar">
|
||||
toolbar="toolbar"
|
||||
mct-selectable="{
|
||||
item: domainObject.useCapability('adapter'),
|
||||
oldItem: domainObject
|
||||
}"
|
||||
mct-init-select>
|
||||
</mct-representation>
|
||||
</div><!--/ l-object-wrapper-inner -->
|
||||
</div>
|
||||
|
@ -25,7 +25,7 @@
|
||||
ng-model="filterBy">
|
||||
</mct-include>
|
||||
<div class="flex-elem grows vscroll">
|
||||
<ul class="tree">
|
||||
<ul class="tree" ng-if="composition.length > 0">
|
||||
<li ng-repeat="containedObject in composition | filter:searchElements">
|
||||
<span class="tree-item">
|
||||
<mct-representation
|
||||
@ -36,5 +36,6 @@
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div ng-if="composition.length === 0">No contained elements</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -101,10 +101,15 @@ define(
|
||||
*/
|
||||
EditorCapability.prototype.finish = function () {
|
||||
var domainObject = this.domainObject;
|
||||
return this.transactionService.cancel().then(function () {
|
||||
domainObject.getCapability("status").set("editing", false);
|
||||
return domainObject;
|
||||
});
|
||||
|
||||
if (this.transactionService.isActive()) {
|
||||
return this.transactionService.cancel().then(function () {
|
||||
domainObject.getCapability("status").set("editing", false);
|
||||
return domainObject;
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve(domainObject);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -28,16 +28,6 @@ define(
|
||||
[],
|
||||
function () {
|
||||
|
||||
function isDirty(domainObject) {
|
||||
var navigatedObject = domainObject,
|
||||
editorCapability = navigatedObject &&
|
||||
navigatedObject.getCapability("editor");
|
||||
|
||||
return editorCapability &&
|
||||
editorCapability.isEditContextRoot() &&
|
||||
editorCapability.dirty();
|
||||
}
|
||||
|
||||
function cancelEditing(domainObject) {
|
||||
var navigatedObject = domainObject,
|
||||
editorCapability = navigatedObject &&
|
||||
@ -59,10 +49,7 @@ define(
|
||||
|
||||
var removeCheck = navigationService
|
||||
.checkBeforeNavigation(function () {
|
||||
if (isDirty(domainObject)) {
|
||||
return "Continuing will cause the loss of any unsaved changes.";
|
||||
}
|
||||
return false;
|
||||
return "Continuing will cause the loss of any unsaved changes.";
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
|
@ -29,7 +29,11 @@ define(
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function ElementsController($scope) {
|
||||
function ElementsController($scope, openmct) {
|
||||
this.scope = $scope;
|
||||
this.scope.composition = [];
|
||||
var self = this;
|
||||
|
||||
function filterBy(text) {
|
||||
if (typeof text === 'undefined') {
|
||||
return $scope.searchText;
|
||||
@ -47,10 +51,54 @@ define(
|
||||
}
|
||||
}
|
||||
|
||||
function setSelection(selection) {
|
||||
if (!selection[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.mutationListener) {
|
||||
self.mutationListener();
|
||||
delete self.mutationListener;
|
||||
}
|
||||
|
||||
var domainObject = selection[0].context.oldItem;
|
||||
self.refreshComposition(domainObject);
|
||||
|
||||
if (domainObject) {
|
||||
self.mutationListener = domainObject.getCapability('mutation')
|
||||
.listen(self.refreshComposition.bind(self, domainObject));
|
||||
}
|
||||
}
|
||||
|
||||
$scope.filterBy = filterBy;
|
||||
$scope.searchElements = searchElements;
|
||||
|
||||
openmct.selection.on('change', setSelection);
|
||||
setSelection(openmct.selection.get());
|
||||
|
||||
$scope.$on("$destroy", function () {
|
||||
openmct.selection.off("change", setSelection);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the composition for the selected object and populates the scope with it.
|
||||
*
|
||||
* @param domainObject the selected object
|
||||
* @private
|
||||
*/
|
||||
ElementsController.prototype.refreshComposition = function (domainObject) {
|
||||
var selectedObjectComposition = domainObject && domainObject.useCapability('composition');
|
||||
|
||||
if (selectedObjectComposition) {
|
||||
selectedObjectComposition.then(function (composition) {
|
||||
this.scope.composition = composition;
|
||||
}.bind(this));
|
||||
} else {
|
||||
this.scope.composition = [];
|
||||
}
|
||||
};
|
||||
|
||||
return ElementsController;
|
||||
}
|
||||
);
|
||||
|
@ -38,7 +38,7 @@ define(
|
||||
* @constructor
|
||||
* @implements {Representer}
|
||||
*/
|
||||
function EditToolbarRepresenter(scope, element, attrs) {
|
||||
function EditToolbarRepresenter(openmct, scope, element, attrs) {
|
||||
var self = this;
|
||||
|
||||
// Mark changes as ready to persist
|
||||
@ -109,6 +109,7 @@ define(
|
||||
this.updateSelection = updateSelection;
|
||||
this.toolbar = undefined;
|
||||
this.toolbarObject = {};
|
||||
this.openmct = openmct;
|
||||
|
||||
// If this representation exposes a toolbar, set up watches
|
||||
// to synchronize with it.
|
||||
@ -146,7 +147,7 @@ define(
|
||||
// Expose the toolbar object to the parent scope
|
||||
initialize(definition);
|
||||
// Create a selection scope
|
||||
this.setSelection(new EditToolbarSelection());
|
||||
this.setSelection(new EditToolbarSelection(this.openmct));
|
||||
// Initialize toolbar to an empty selection
|
||||
this.updateSelection([]);
|
||||
};
|
||||
|
@ -38,10 +38,24 @@ define(
|
||||
* @memberof platform/commonUI/edit
|
||||
* @constructor
|
||||
*/
|
||||
function EditToolbarSelection() {
|
||||
function EditToolbarSelection(openmct) {
|
||||
this.selection = [{}];
|
||||
this.selecting = false;
|
||||
this.selectedObj = undefined;
|
||||
|
||||
openmct.selection.on('change', function (selection) {
|
||||
var selected = selection[0];
|
||||
|
||||
if (selected && selected.context.toolbar) {
|
||||
this.select(selected.context.toolbar);
|
||||
} else {
|
||||
this.deselect();
|
||||
}
|
||||
|
||||
if (selected && selected.context.viewProxy) {
|
||||
this.proxy(selected.context.viewProxy);
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -62,6 +62,7 @@ define(
|
||||
);
|
||||
mockTransactionService.commit.andReturn(fastPromise());
|
||||
mockTransactionService.cancel.andReturn(fastPromise());
|
||||
mockTransactionService.isActive = jasmine.createSpy('isActive');
|
||||
|
||||
mockStatusCapability = jasmine.createSpyObj(
|
||||
"statusCapability",
|
||||
@ -141,6 +142,7 @@ define(
|
||||
|
||||
describe("finish", function () {
|
||||
beforeEach(function () {
|
||||
mockTransactionService.isActive.andReturn(true);
|
||||
capability.edit();
|
||||
capability.finish();
|
||||
});
|
||||
@ -152,6 +154,23 @@ define(
|
||||
});
|
||||
});
|
||||
|
||||
describe("finish", function () {
|
||||
beforeEach(function () {
|
||||
mockTransactionService.isActive.andReturn(false);
|
||||
capability.edit();
|
||||
});
|
||||
|
||||
it("does not cancel transaction when transaction is not active", function () {
|
||||
capability.finish();
|
||||
expect(mockTransactionService.cancel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns a promise", function () {
|
||||
expect(capability.finish() instanceof Promise).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("dirty", function () {
|
||||
var model = {};
|
||||
|
||||
|
@ -104,10 +104,10 @@ define(
|
||||
mockEditorCapability.isEditContextRoot.andReturn(false);
|
||||
mockEditorCapability.dirty.andReturn(false);
|
||||
|
||||
expect(checkFn()).toBe(false);
|
||||
expect(checkFn()).toBe("Continuing will cause the loss of any unsaved changes.");
|
||||
|
||||
mockEditorCapability.isEditContextRoot.andReturn(true);
|
||||
expect(checkFn()).toBe(false);
|
||||
expect(checkFn()).toBe("Continuing will cause the loss of any unsaved changes.");
|
||||
|
||||
mockEditorCapability.dirty.andReturn(true);
|
||||
expect(checkFn())
|
||||
|
@ -27,11 +27,47 @@ define(
|
||||
|
||||
describe("The Elements Pane controller", function () {
|
||||
var mockScope,
|
||||
mockOpenMCT,
|
||||
mockSelection,
|
||||
mockDomainObject,
|
||||
mockMutationCapability,
|
||||
mockUnlisten,
|
||||
selectable = [],
|
||||
controller;
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpy("$scope");
|
||||
controller = new ElementsController(mockScope);
|
||||
mockUnlisten = jasmine.createSpy('unlisten');
|
||||
mockMutationCapability = jasmine.createSpyObj("mutationCapability", [
|
||||
"listen"
|
||||
]);
|
||||
mockMutationCapability.listen.andReturn(mockUnlisten);
|
||||
mockDomainObject = jasmine.createSpyObj("domainObject", [
|
||||
"getCapability",
|
||||
"useCapability"
|
||||
]);
|
||||
mockDomainObject.useCapability.andCallThrough();
|
||||
mockDomainObject.getCapability.andReturn(mockMutationCapability);
|
||||
|
||||
mockScope = jasmine.createSpyObj("$scope", ['$on']);
|
||||
mockSelection = jasmine.createSpyObj("selection", [
|
||||
'on',
|
||||
'off',
|
||||
'get'
|
||||
]);
|
||||
mockSelection.get.andReturn([]);
|
||||
mockOpenMCT = {
|
||||
selection: mockSelection
|
||||
};
|
||||
|
||||
selectable[0] = {
|
||||
context: {
|
||||
oldItem: mockDomainObject
|
||||
}
|
||||
};
|
||||
|
||||
spyOn(ElementsController.prototype, 'refreshComposition');
|
||||
|
||||
controller = new ElementsController(mockScope, mockOpenMCT);
|
||||
});
|
||||
|
||||
function getModel(model) {
|
||||
@ -63,6 +99,44 @@ define(
|
||||
expect(objects.filter(mockScope.searchElements).length).toBe(4);
|
||||
});
|
||||
|
||||
it("refreshes composition on selection", function () {
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(ElementsController.prototype.refreshComposition).toHaveBeenCalledWith(mockDomainObject);
|
||||
});
|
||||
|
||||
it("listens on mutation and refreshes composition", function () {
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(mockDomainObject.getCapability).toHaveBeenCalledWith('mutation');
|
||||
expect(mockMutationCapability.listen).toHaveBeenCalled();
|
||||
expect(ElementsController.prototype.refreshComposition.calls.length).toBe(1);
|
||||
|
||||
mockMutationCapability.listen.mostRecentCall.args[0](mockDomainObject);
|
||||
|
||||
expect(ElementsController.prototype.refreshComposition.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it("cleans up mutation listener when selection changes", function () {
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(mockMutationCapability.listen).toHaveBeenCalled();
|
||||
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(mockUnlisten).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not listen on mutation for element proxy selectable", function () {
|
||||
selectable[0] = {
|
||||
context: {
|
||||
elementProxy: {}
|
||||
}
|
||||
};
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(mockDomainObject.getCapability).not.toHaveBeenCalledWith('mutation');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -29,7 +29,9 @@ define(
|
||||
mockElement,
|
||||
testAttrs,
|
||||
mockUnwatch,
|
||||
representer;
|
||||
representer,
|
||||
mockOpenMCT,
|
||||
mockSelection;
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
@ -46,7 +48,18 @@ define(
|
||||
|
||||
mockScope.$parent.$watchCollection.andReturn(mockUnwatch);
|
||||
|
||||
mockSelection = jasmine.createSpyObj("selection", [
|
||||
'on',
|
||||
'off',
|
||||
'get'
|
||||
]);
|
||||
mockSelection.get.andReturn([]);
|
||||
mockOpenMCT = {
|
||||
selection: mockSelection
|
||||
};
|
||||
|
||||
representer = new EditToolbarRepresenter(
|
||||
mockOpenMCT,
|
||||
mockScope,
|
||||
mockElement,
|
||||
testAttrs
|
||||
|
@ -28,13 +28,25 @@ define(
|
||||
var testProxy,
|
||||
testElement,
|
||||
otherElement,
|
||||
selection;
|
||||
selection,
|
||||
mockSelection,
|
||||
mockOpenMCT;
|
||||
|
||||
beforeEach(function () {
|
||||
testProxy = { someKey: "some value" };
|
||||
testElement = { someOtherKey: "some other value" };
|
||||
otherElement = { yetAnotherKey: 42 };
|
||||
selection = new EditToolbarSelection();
|
||||
mockSelection = jasmine.createSpyObj("selection", [
|
||||
// 'select',
|
||||
'on',
|
||||
'off',
|
||||
'get'
|
||||
]);
|
||||
mockSelection.get.andReturn([]);
|
||||
mockOpenMCT = {
|
||||
selection: mockSelection
|
||||
};
|
||||
selection = new EditToolbarSelection(mockOpenMCT);
|
||||
selection.proxy(testProxy);
|
||||
});
|
||||
|
||||
|
@ -121,6 +121,9 @@ define([
|
||||
};
|
||||
|
||||
UTCTimeFormat.prototype.parse = function (text) {
|
||||
if (typeof text === 'number') {
|
||||
return text;
|
||||
}
|
||||
return moment.utc(text, DATE_FORMATS).valueOf();
|
||||
};
|
||||
|
||||
|
@ -41,6 +41,7 @@ define([
|
||||
"./src/controllers/BannerController",
|
||||
"./src/directives/MCTContainer",
|
||||
"./src/directives/MCTDrag",
|
||||
"./src/directives/MCTSelectable",
|
||||
"./src/directives/MCTClickElsewhere",
|
||||
"./src/directives/MCTResize",
|
||||
"./src/directives/MCTPopup",
|
||||
@ -90,6 +91,7 @@ define([
|
||||
BannerController,
|
||||
MCTContainer,
|
||||
MCTDrag,
|
||||
MCTSelectable,
|
||||
MCTClickElsewhere,
|
||||
MCTResize,
|
||||
MCTPopup,
|
||||
@ -328,6 +330,13 @@ define([
|
||||
"$document"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "mctSelectable",
|
||||
"implementation": MCTSelectable,
|
||||
"depends": [
|
||||
"openmct"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "mctClickElsewhere",
|
||||
"implementation": MCTClickElsewhere,
|
||||
|
@ -137,6 +137,11 @@
|
||||
min-height: 0;
|
||||
&.holder:not(:last-child) { margin-bottom: $interiorMarginLg; }
|
||||
}
|
||||
&.l-flex-accordion .flex-accordion-holder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
//overflow: hidden !important;
|
||||
}
|
||||
.flex-container { @include flex-direction(column); }
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ $plotXBarH: 32px;
|
||||
$plotLegendH: 20px;
|
||||
$plotSwatchD: 8px;
|
||||
// 1: Top, 2: right, 3: bottom, 4: left
|
||||
$plotDisplayArea: ($plotLegendH + $interiorMargin, 0, $plotXBarH, $plotYBarW);
|
||||
$plotDisplayArea: (0, 0, $plotXBarH, $plotYBarW);
|
||||
/* min plot height is based on user testing to find minimum useful height */
|
||||
$plotMinH: 95px;
|
||||
/*************** Bubbles */
|
||||
|
@ -25,6 +25,7 @@
|
||||
}
|
||||
|
||||
.l-fixed-position-item {
|
||||
border-width: 1px;
|
||||
position: absolute;
|
||||
&.s-not-selected {
|
||||
opacity: 0.8;
|
||||
|
@ -40,7 +40,7 @@
|
||||
* Use https://icomoon.io/app with icomoon-project-openmct-symbols-12px.json
|
||||
* to generate font files
|
||||
*/
|
||||
font-family: 'symbolsfont 12px';
|
||||
font-family: 'symbolsfont-12px';
|
||||
src: url($dirCommonRes + 'fonts/symbols/openmct-symbols-12px.eot');
|
||||
src: url($dirCommonRes + 'fonts/symbols/openmct-symbols-12px.eot?#iefix') format('embedded-opentype'),
|
||||
url($dirCommonRes + 'fonts/symbols/openmct-symbols-12px.woff') format('woff'),
|
||||
@ -180,6 +180,20 @@ a.disabled {
|
||||
@include ellipsize();
|
||||
}
|
||||
|
||||
.no-selection {
|
||||
// aka selection = "None". Used in palettes and their menu buttons.
|
||||
$c: red; $s: 48%; $e: 52%;
|
||||
@include background-image(linear-gradient(-45deg,
|
||||
transparent $s - 5%,
|
||||
$c $s,
|
||||
$c $e,
|
||||
transparent $e + 5%
|
||||
));
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
|
||||
.scrolling,
|
||||
.scroll {
|
||||
overflow: auto;
|
||||
@ -234,6 +248,12 @@ a.disabled {
|
||||
color: rgba(#fff, 0.2);
|
||||
}
|
||||
|
||||
.comma-list span {
|
||||
&:not(:first-child) {
|
||||
&:before { content: ', '; }
|
||||
}
|
||||
}
|
||||
|
||||
.test-stripes {
|
||||
@include bgDiagonalStripes();
|
||||
}
|
||||
|
@ -44,6 +44,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.t-alert-unsynced {
|
||||
@extend .icon-alert-triangle;
|
||||
color: $colorPausedBg;
|
||||
}
|
||||
|
||||
.bar .ui-symbol {
|
||||
display: inline-block;
|
||||
}
|
||||
@ -81,18 +87,5 @@
|
||||
@include transform(scale(0.3));
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* .t-item-icon-glyph {
|
||||
&:after {
|
||||
color: $colorIconLink;
|
||||
content: '\e921'; //$glyph-icon-link;
|
||||
height: auto; width: auto;
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 20%;
|
||||
@include transform-origin(bottom left);
|
||||
@include transform(scale(0.3));
|
||||
z-index: 2;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +53,7 @@
|
||||
.l-inspector-part {
|
||||
box-sizing: border-box;
|
||||
padding-right: $interiorMargin;
|
||||
|
||||
.tree .form {
|
||||
margin-left: $treeVCW + $interiorMarginLg;
|
||||
}
|
||||
@ -78,6 +79,7 @@
|
||||
}
|
||||
}
|
||||
.form-row {
|
||||
// To be replaced with .inspector-config, see below.
|
||||
@include align-items(center);
|
||||
border: none !important;
|
||||
margin-bottom: 0 !important;
|
||||
@ -99,15 +101,12 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ul li {
|
||||
margin-bottom: $interiorMarginLg;
|
||||
}
|
||||
|
||||
em.t-inspector-part-header {
|
||||
border-radius: $basicCr;
|
||||
background-color: $colorInspectorSectionHeaderBg;
|
||||
color: $colorInspectorSectionHeaderFg;
|
||||
margin-bottom: $interiorMargin;
|
||||
margin-top: $interiorMarginLg;
|
||||
//margin-bottom: $interiorMargin;
|
||||
padding: floor($formTBPad * .75) $formLRPad;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@ -201,3 +200,102 @@ mct-representation:not(.s-status-editing) .l-inspect {
|
||||
pointer-events: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
// NEW COMPACT FORM, FOR USE IN INSPECTOR
|
||||
// ul > li > label, control
|
||||
// Make a new UL for each form section
|
||||
// Allow control-first, controls-below
|
||||
|
||||
.l-inspect .tree ul li,
|
||||
.inspector-config ul li {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
|
||||
.inspector-config {
|
||||
$labelW: 40%;
|
||||
$minW: $labelW;
|
||||
ul {
|
||||
margin-bottom: $interiorMarginLg;
|
||||
li {
|
||||
@include display(flex);
|
||||
@include flex-wrap(wrap);
|
||||
@include align-items(center);
|
||||
label,
|
||||
.control {
|
||||
@include display(flex);
|
||||
min-width: $minW;
|
||||
}
|
||||
label {
|
||||
line-height: inherit;
|
||||
padding: $interiorMarginSm 0;
|
||||
width: $labelW;
|
||||
}
|
||||
.control {
|
||||
@include flex-grow(1);
|
||||
}
|
||||
|
||||
&:not(.section-header) {
|
||||
&:not(.connects-to-previous) {
|
||||
//border-top: 1px solid $colorFormLines;
|
||||
}
|
||||
}
|
||||
|
||||
&.connects-to-previous {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
&.section-header {
|
||||
margin-top: $interiorMarginLg;
|
||||
border-top: 1px solid $colorFormLines;
|
||||
}
|
||||
|
||||
&.controls-first {
|
||||
.control {
|
||||
@include flex-grow(0);
|
||||
margin-right: $interiorMargin;
|
||||
min-width: 0;
|
||||
order: 1;
|
||||
width: auto;
|
||||
}
|
||||
label {
|
||||
@include flex-grow(1);
|
||||
order: 2;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
&.controls-under {
|
||||
display: block;
|
||||
.control, label {
|
||||
display: block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
ul li {
|
||||
border-top: none !important;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-error {
|
||||
// Block element that visually flags an error and contains a message
|
||||
background-color: $colorFormFieldErrorBg;
|
||||
color: $colorFormFieldErrorFg;
|
||||
border-radius: $basicCr;
|
||||
display: block;
|
||||
padding: 1px 6px;
|
||||
&:before {
|
||||
content: $glyph-icon-alert-triangle;
|
||||
display: inline;
|
||||
font-family: symbolsfont;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tree .inspector-config {
|
||||
margin-left: $treeVCW + $interiorMarginLg;
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@
|
||||
/********************************* CONTROLS */
|
||||
@import "controls/breadcrumb";
|
||||
@import "controls/buttons";
|
||||
@import "controls/color-palette";
|
||||
@import "controls/palette";
|
||||
@import "controls/controls";
|
||||
@import "controls/lists";
|
||||
@import "controls/menus";
|
||||
@ -70,6 +70,7 @@
|
||||
@import "fixed-position";
|
||||
@import "lists/tabular";
|
||||
@import "plots/plots-main";
|
||||
@import "plots/legend";
|
||||
@import "iframe";
|
||||
@import "views";
|
||||
@import "items/item";
|
||||
@ -80,3 +81,4 @@
|
||||
@import "autoflow";
|
||||
@import "features/imagery";
|
||||
@import "features/time-display";
|
||||
@import "widgets";
|
||||
|
@ -5,6 +5,7 @@
|
||||
}
|
||||
|
||||
.l-view-section {
|
||||
//@include test(orange, 0.1);
|
||||
@include absPosDefault(0);
|
||||
h2 {
|
||||
color: #fff;
|
||||
|
306
platform/commonUI/general/res/sass/_widgets.scss
Normal file
306
platform/commonUI/general/res/sass/_widgets.scss
Normal file
@ -0,0 +1,306 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2017, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/************************************************************* WIDGET OBJECT */
|
||||
.l-summary-widget {
|
||||
// Widget layout classes here
|
||||
@include ellipsize();
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
.widget-label:before {
|
||||
// Widget icon
|
||||
font-size: 0.9em;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
.s-summary-widget {
|
||||
// Widget style classes here
|
||||
@include boxShdw($shdwBtns);
|
||||
border-radius: $basicCr;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
box-sizing: border-box;
|
||||
cursor: default;
|
||||
font-size: 0.8rem;
|
||||
padding: $interiorMarginLg $interiorMarginLg * 2;
|
||||
&[href] {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-edit-holder {
|
||||
// Hide edit area when in browse mode
|
||||
display: none;
|
||||
}
|
||||
|
||||
.widget-rule-header {
|
||||
@extend .l-flex-row;
|
||||
@include align-items(center);
|
||||
margin-bottom: $interiorMargin;
|
||||
> .flex-elem {
|
||||
&:not(:first-child) {
|
||||
margin-left: $interiorMargin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.widget-rules-wrapper,
|
||||
.widget-rule-content,
|
||||
.w-widget-test-data-content {
|
||||
@include trans-prop-nice($props: (height, min-height, opacity), $dur: 250ms);
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.widget-rules-wrapper {
|
||||
flex: 1 1 auto !important;
|
||||
}
|
||||
|
||||
.widget-rule-content.expanded {
|
||||
overflow: visible !important;
|
||||
min-height: 50px;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
pointer-events: inherit;
|
||||
}
|
||||
|
||||
.w-widget-test-data-content {
|
||||
.l-enable {
|
||||
padding: $interiorMargin 0;
|
||||
}
|
||||
|
||||
.w-widget-test-data-items {
|
||||
max-height: 20vh;
|
||||
overflow-y: scroll !important;
|
||||
padding-right: $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
.l-widget-thumb-wrapper,
|
||||
.l-compact-form label {
|
||||
$ruleLabelW: 40%;
|
||||
$ruleLabelMaxW: 150px;
|
||||
@include display(flex);
|
||||
max-width: $ruleLabelMaxW;
|
||||
width: $ruleLabelW;
|
||||
}
|
||||
|
||||
.t-message-widget-no-data {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/********************************************************** EDITING A WIDGET */
|
||||
.s-status-editing > mct-view > .w-summary-widget {
|
||||
// Classes for editor layout while editing a widget
|
||||
// This selector is ugly and brittle, but needed to prevent interface from showing when widget is in a layout
|
||||
// being edited.
|
||||
@include absPosDefault();
|
||||
@extend .l-flex-col;
|
||||
|
||||
> .l-summary-widget {
|
||||
// Main view of the summary widget
|
||||
// Give some airspace and center the widget in the area
|
||||
margin: 30px auto;
|
||||
}
|
||||
|
||||
.widget-edit-holder {
|
||||
display: flex; // Overrides `display: none` during Browse mode
|
||||
.flex-accordion-holder {
|
||||
// Needed because otherwise accordion elements "creep" when contents expand and contract
|
||||
display: block !important;
|
||||
}
|
||||
&.expanded-widget-test-data {
|
||||
.w-widget-test-data-content {
|
||||
min-height: 50px;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
pointer-events: inherit;
|
||||
}
|
||||
&:not(.expanded-widget-rules) {
|
||||
// Test data is expanded and rules are collapsed
|
||||
// Make text data take up all the vertical space
|
||||
.flex-accordion-holder { display: flex; }
|
||||
.widget-test-data {
|
||||
flex-grow: 999999;
|
||||
}
|
||||
.w-widget-test-data-items {
|
||||
max-height: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.expanded-widget-rules {
|
||||
.widget-rules-wrapper {
|
||||
min-height: 50px;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
pointer-events: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.s-status-no-data {
|
||||
.widget-edit-holder {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
.t-message-widget-no-data {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.l-compact-form {
|
||||
// Overrides on .l-compact-form
|
||||
ul {
|
||||
&:last-child { margin: 0; }
|
||||
li {
|
||||
@include align-items(flex-start);
|
||||
@include flex-wrap(nowrap);
|
||||
line-height: 230%; // Provide enough space when controls wrap
|
||||
padding: 2px 0;
|
||||
&:not(.widget-rule-header) {
|
||||
&:not(.connects-to-previous) {
|
||||
border-top: 1px solid $colorFormLines;
|
||||
}
|
||||
}
|
||||
&.connects-to-previous {
|
||||
padding: $interiorMargin 0;
|
||||
}
|
||||
|
||||
> label {
|
||||
display: block; // Needed to align text to right
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.s-widget-test-data-item {
|
||||
// Single line of ul li label span, etc.
|
||||
ul {
|
||||
li {
|
||||
border: none !important;
|
||||
> label {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.widget-edit-holder {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.widget-rules-wrapper {
|
||||
// Wrapper area that holds n rules
|
||||
box-sizing: border-box;
|
||||
overflow-y: scroll;
|
||||
padding-right: $interiorMargin;
|
||||
}
|
||||
|
||||
.l-widget-rule,
|
||||
.l-widget-test-data-item {
|
||||
box-sizing: border-box;
|
||||
margin-bottom: $interiorMarginSm;
|
||||
padding: $interiorMargin $interiorMarginLg;
|
||||
}
|
||||
|
||||
.l-widget-thumb-wrapper {
|
||||
@extend .l-flex-row;
|
||||
@include align-items(center);
|
||||
> span { display: block; }
|
||||
.grippy-holder,
|
||||
.view-control {
|
||||
margin-right: $interiorMargin;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.widget-thumb {
|
||||
@include flex(1 1 auto);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.rule-title {
|
||||
@include flex(0 1 auto);
|
||||
color: pullForward($colorBodyFg, 50%);
|
||||
}
|
||||
|
||||
.rule-description {
|
||||
@include flex(1 1 auto);
|
||||
@include ellipsize();
|
||||
color: pushBack($colorBodyFg, 20%);
|
||||
}
|
||||
|
||||
.s-widget-rule,
|
||||
.s-widget-test-data-item {
|
||||
background-color: rgba($colorBodyFg, 0.1);
|
||||
border-radius: $basicCr;
|
||||
}
|
||||
|
||||
.widget-thumb {
|
||||
@include ellipsize();
|
||||
@extend .s-summary-widget;
|
||||
@extend .l-summary-widget;
|
||||
padding: $interiorMarginSm $interiorMargin;
|
||||
}
|
||||
|
||||
// Hide and show elements in the rule-header on hover
|
||||
.l-widget-rule,
|
||||
.l-widget-test-data-item {
|
||||
.grippy,
|
||||
.l-rule-action-buttons-wrapper,
|
||||
.l-condition-action-buttons-wrapper,
|
||||
.l-widget-test-data-item-action-buttons-wrapper {
|
||||
@include trans-prop-nice($props: opacity, $dur: 500ms);
|
||||
opacity: 0;
|
||||
}
|
||||
&:hover {
|
||||
.grippy,
|
||||
.l-rule-action-buttons-wrapper,
|
||||
.l-widget-test-data-item-action-buttons-wrapper {
|
||||
@include trans-prop-nice($props: opacity, $dur: 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.l-rule-action-buttons-wrapper {
|
||||
.t-delete {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.t-condition {
|
||||
&:hover {
|
||||
.l-condition-action-buttons-wrapper {
|
||||
@include trans-prop-nice($props: opacity, $dur: 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -150,6 +150,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************** VIEW CONTROLS */
|
||||
// Expand/collapse > and v arrows, used in tree and plot legend
|
||||
// Moved this over from a tree-only context 5/18/17
|
||||
|
||||
.view-control {
|
||||
@extend .ui-symbol;
|
||||
cursor: pointer;
|
||||
height: 1em; width: 1em;
|
||||
line-height: inherit;
|
||||
&:before {
|
||||
position: absolute;
|
||||
@include trans-prop-nice(transform, 100ms);
|
||||
content: $glyph-icon-arrow-right;
|
||||
@include transform-origin(center);
|
||||
}
|
||||
&.expanded:before {
|
||||
@include transform(rotate(90deg));
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************** CUSTOM CHECKBOXES */
|
||||
label.checkbox.custom,
|
||||
label.radio.custom {
|
||||
@ -261,7 +281,7 @@ input[type="number"] {
|
||||
input[type="text"].lg { width: 100% !important; }
|
||||
.l-input-med input[type="text"],
|
||||
input[type="text"].med { width: 200px !important; }
|
||||
input[type="text"].sm { width: 50px !important; }
|
||||
input[type="text"].sm, input[type="number"].sm { width: 50px !important; }
|
||||
.l-numeric input[type="text"],
|
||||
input[type="text"].numeric { text-align: right; }
|
||||
|
||||
@ -317,14 +337,10 @@ input[type="text"].s-input-inline,
|
||||
.select {
|
||||
@include btnSubtle($bg: $colorSelectBg);
|
||||
@extend .icon-arrow-down; // Context arrow
|
||||
@if $shdwBtns != none {
|
||||
margin: 0 0 2px 0; // Needed to avoid dropshadow from being clipped by parent containers
|
||||
}
|
||||
display: inline-block;
|
||||
padding: 0 $interiorMargin;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
line-height: $formInputH;
|
||||
select {
|
||||
@include appearance(none);
|
||||
box-sizing: border-box;
|
||||
@ -340,11 +356,13 @@ input[type="text"].s-input-inline,
|
||||
}
|
||||
}
|
||||
&:before {
|
||||
pointer-events: none;
|
||||
@include transform(translateY(-50%));
|
||||
color: rgba($colorInvokeMenu, percentToDecimal($contrastInvokeMenuPercent));
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: $interiorMargin; top: 0;
|
||||
right: $interiorMargin;
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -396,8 +414,7 @@ input[type="text"].s-input-inline,
|
||||
.l-elem-wrapper {
|
||||
mct-representation {
|
||||
// Holds the context-available item
|
||||
// Must have min-width to make flex work properly
|
||||
// in Safari
|
||||
// Must have min-width to make flex work properly in Safari
|
||||
min-width: 0.7em;
|
||||
}
|
||||
}
|
||||
@ -563,7 +580,6 @@ input[type="text"].s-input-inline,
|
||||
height: $h;
|
||||
margin-top: 1 + floor($h/2) * -1;
|
||||
@include btnSubtle(pullForward($colorBtnBg, 10%));
|
||||
//border-radius: 50% !important;
|
||||
}
|
||||
|
||||
@mixin sliderKnobRound() {
|
||||
@ -578,7 +594,6 @@ input[type="text"].s-input-inline,
|
||||
|
||||
input[type="range"] {
|
||||
// HTML5 range inputs
|
||||
|
||||
-webkit-appearance: none; /* Hides the slider so that custom slider can be made */
|
||||
background: transparent; /* Otherwise white in Chrome */
|
||||
&:focus {
|
||||
@ -736,6 +751,30 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
.view-switcher,
|
||||
.t-btn-view-large {
|
||||
@include trans-prop-nice-fade($controlFadeMs);
|
||||
}
|
||||
|
||||
.view-control {
|
||||
@extend .icon-arrow-right;
|
||||
cursor: pointer;
|
||||
font-size: 0.75em;
|
||||
&:before {
|
||||
position: absolute;
|
||||
@include trans-prop-nice(transform, 100ms);
|
||||
@include transform-origin(center);
|
||||
}
|
||||
&.expanded:before {
|
||||
@include transform(rotate(90deg));
|
||||
}
|
||||
}
|
||||
|
||||
.grippy {
|
||||
@extend .icon-grippy;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
/******************************************************** BROWSER ELEMENTS */
|
||||
body.desktop {
|
||||
::-webkit-scrollbar {
|
||||
|
@ -29,23 +29,27 @@
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px; //120%;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.title-label {
|
||||
margin-left: $interiorMarginSm;
|
||||
}
|
||||
|
||||
.icon-swatch,
|
||||
.color-swatch {
|
||||
// Used in color menu buttons in toolbar
|
||||
$d: 10px;
|
||||
display: inline-block;
|
||||
border: 1px solid rgba($colorBtnFg, 0.2);
|
||||
height: $d;
|
||||
width: $d;
|
||||
height: $d; width: $d;
|
||||
line-height: $d;
|
||||
vertical-align: middle;
|
||||
margin-left: $interiorMarginSm;
|
||||
margin-top: -2px;
|
||||
&:not(.no-selection) {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
|
@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/******************************************************************* STATUS BLOCK ELEMS */
|
||||
@mixin statusBannerColors($bg, $fg: $colorStatusFg) {
|
||||
$bgPb: 30%;
|
||||
$bgPbD: 10%;
|
||||
@ -140,7 +140,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Styles for messages and message banners */
|
||||
/******************************************************************* MESSAGE BANNERS */
|
||||
.message {
|
||||
&.block {
|
||||
border-radius: $basicCr;
|
||||
@ -196,7 +196,6 @@
|
||||
padding: 0 $interiorMargin;
|
||||
}
|
||||
.close {
|
||||
//@include test(red, 0.7);
|
||||
cursor: pointer;
|
||||
font-size: 7px;
|
||||
width: 8px;
|
||||
@ -240,132 +239,147 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin messageBlock($iconW: 32px) {
|
||||
.type-icon.message-type {
|
||||
/******************************************************************* MESSAGES */
|
||||
|
||||
/* Contexts:
|
||||
In .t-message-list
|
||||
In .overlay as a singleton
|
||||
Inline in the view area
|
||||
*/
|
||||
|
||||
// Archetypal message
|
||||
.l-message {
|
||||
$iconW: 32px;
|
||||
@include display(flex);
|
||||
@include flex-direction(row);
|
||||
@include align-items(stretch);
|
||||
padding: $interiorMarginLg;
|
||||
|
||||
&:before {
|
||||
// Icon
|
||||
@include flex(0 1 auto);
|
||||
@include txtShdw($shdwStatusIc);
|
||||
@extend .icon-bell;
|
||||
color: $colorStatusDefault;
|
||||
font-size: $iconW;
|
||||
padding: 1px;
|
||||
width: $iconW + 2;
|
||||
margin-right: $interiorMarginLg;
|
||||
}
|
||||
|
||||
.message-severity-info .type-icon.message-type {
|
||||
&.message-severity-info:before {
|
||||
@extend .icon-info;
|
||||
color: $colorInfo;
|
||||
}
|
||||
.message-severity-alert .type-icon.message-type {
|
||||
@extend .icon-bell;
|
||||
|
||||
&.message-severity-alert:before {
|
||||
color: $colorWarningLo;
|
||||
}
|
||||
.message-severity-error .type-icon.message-type {
|
||||
|
||||
&.message-severity-error:before {
|
||||
@extend .icon-alert-rect;
|
||||
color: $colorWarningHi;
|
||||
}
|
||||
}
|
||||
/* Paths:
|
||||
t-dialog | t-dialog-sm > t-message-single | t-message-list > overlay > holder > contents > l-message >
|
||||
message-type > (icon)
|
||||
message-contents >
|
||||
top-bar >
|
||||
title
|
||||
hint
|
||||
editor >
|
||||
(if displaying list of messages)
|
||||
ul > li > l-message >
|
||||
... same as above
|
||||
bottom-bar
|
||||
*/
|
||||
|
||||
.l-message {
|
||||
|
||||
.w-message-contents {
|
||||
@include flex(1 1 auto);
|
||||
@include display(flex);
|
||||
@include flex-direction(row);
|
||||
@include align-items(stretch);
|
||||
.type-icon.message-type {
|
||||
@include flex(0 1 auto);
|
||||
position: relative;
|
||||
}
|
||||
.message-contents {
|
||||
@include flex(1 1 auto);
|
||||
margin-left: $overlayMargin;
|
||||
position: relative;
|
||||
@include flex-direction(column);
|
||||
|
||||
.top-bar,
|
||||
> div,
|
||||
> span {
|
||||
//@include test(red);
|
||||
margin-bottom: $interiorMargin;
|
||||
}
|
||||
|
||||
.message-body {
|
||||
@include flex(1 1 100%);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton in an overlay dialog
|
||||
.t-message-single .l-message,
|
||||
.t-message-single.l-message {
|
||||
$iconW: 80px;
|
||||
@include absPosDefault();
|
||||
padding: 0;
|
||||
&:before {
|
||||
font-size: $iconW;
|
||||
width: $iconW + 2;
|
||||
}
|
||||
.title {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton inline in a view
|
||||
.t-message-inline .l-message,
|
||||
.t-message-inline.l-message {
|
||||
border-radius: $controlCr;
|
||||
&.message-severity-info { background-color: rgba($colorInfo, 0.3); }
|
||||
&.message-severity-alert { background-color: rgba($colorWarningLo, 0.3); }
|
||||
&.message-severity-error { background-color: rgba($colorWarningHi, 0.3); }
|
||||
|
||||
.w-message-contents.l-message-body-only {
|
||||
.message-body {
|
||||
margin-bottom: $interiorMarginLg * 2;
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In a list
|
||||
.t-message-list {
|
||||
@include absPosDefault();
|
||||
@include display(flex);
|
||||
@include flex-direction(column);
|
||||
|
||||
// Message as singleton
|
||||
.t-message-single {
|
||||
@include messageBlock(80px);
|
||||
}
|
||||
|
||||
body.desktop .t-message-single {
|
||||
.l-message,
|
||||
.bottom-bar {
|
||||
@include absPosDefault();
|
||||
> div,
|
||||
> span {
|
||||
margin-bottom: $interiorMargin;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
top: auto;
|
||||
height: $ovrFooterH;
|
||||
.w-messages {
|
||||
@include flex(1 1 100%);
|
||||
overflow-y: auto;
|
||||
padding-right: $interiorMargin;
|
||||
}
|
||||
// Each message
|
||||
.l-message {
|
||||
border-radius: $controlCr;
|
||||
background: rgba($colorOvrFg, 0.1);
|
||||
margin-bottom: $interiorMargin;
|
||||
.hint,
|
||||
.bottom-bar {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@include phonePortrait {
|
||||
.t-message-single {
|
||||
.l-message {
|
||||
@include flex-direction(column);
|
||||
.message-contents { margin-left: 0; }
|
||||
}
|
||||
.type-icon.message-type {
|
||||
.t-message-single .l-message,
|
||||
.t-message-single.l-message {
|
||||
@include flex-direction(column);
|
||||
&:before {
|
||||
margin-right: 0;
|
||||
margin-bottom: $interiorMarginLg;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Messages in list
|
||||
.t-message-list {
|
||||
@include messageBlock(32px);
|
||||
|
||||
.message-contents {
|
||||
.l-message {
|
||||
border-radius: $controlCr;
|
||||
background: rgba($colorOvrFg, 0.1);
|
||||
margin-bottom: $interiorMargin;
|
||||
padding: $interiorMarginLg;
|
||||
|
||||
.message-contents,
|
||||
.bottom-bar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-contents {
|
||||
font-size: 0.9em;
|
||||
margin-left: $interiorMarginLg;
|
||||
.message-action { color: pushBack($colorOvrFg, 20%); }
|
||||
.bottom-bar { text-align: left; }
|
||||
}
|
||||
|
||||
.top-bar,
|
||||
.message-body {
|
||||
margin-bottom: $interiorMarginLg;
|
||||
text-align: center;
|
||||
.s-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.desktop .t-message-list {
|
||||
.message-contents .l-message { margin-right: $interiorMarginLg; }
|
||||
.w-message-contents { padding-right: $interiorMargin; }
|
||||
}
|
||||
|
||||
// Alert elements in views
|
||||
@ -384,10 +398,6 @@ body.desktop .t-message-list {
|
||||
.object-header {
|
||||
.t-object-alert {
|
||||
display: inline;
|
||||
&.t-alert-unsynced {
|
||||
@extend .icon-alert-triangle;
|
||||
color: $colorPausedBg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
89
platform/commonUI/general/res/sass/controls/_palette.scss
Normal file
89
platform/commonUI/general/res/sass/controls/_palette.scss
Normal file
@ -0,0 +1,89 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2017, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
.l-palette {
|
||||
box-sizing: border-box;
|
||||
padding: $interiorMargin !important;
|
||||
}
|
||||
|
||||
.l-palette-row {
|
||||
$d: 16px;
|
||||
$m: 1;
|
||||
$colorsPerRow: 10;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
line-height: $d;
|
||||
width: ($d * $colorsPerRow) + ($m * $colorsPerRow);
|
||||
|
||||
&.l-option-row {
|
||||
margin-bottom: $interiorMargin;
|
||||
.s-palette-item {
|
||||
border-color: $colorPaletteFg;
|
||||
}
|
||||
}
|
||||
|
||||
.l-palette-item {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
height: $d; width: $d;
|
||||
min-width: $d;
|
||||
line-height: $d * 0.9;
|
||||
margin: 0 ($m * 1px) ($m * 1px) 0;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.s-palette-item {
|
||||
border: 1px solid transparent;
|
||||
color: $colorPaletteFg;
|
||||
text-shadow: $shdwPaletteFg;
|
||||
@include trans-prop-nice-fade(0.25s);
|
||||
&:hover {
|
||||
@include trans-prop-nice-fade(0);
|
||||
border-color: $colorPaletteSelected !important;
|
||||
}
|
||||
&.selected {
|
||||
border-color: $colorPaletteSelected;
|
||||
box-shadow: $shdwPaletteSelected; //Needed to see selection rect on light colored swatches
|
||||
}
|
||||
}
|
||||
|
||||
.l-palette-item-label {
|
||||
margin-left: $interiorMargin;
|
||||
}
|
||||
|
||||
.l-inline-palette {
|
||||
.l-palette-row {
|
||||
width: 100%;
|
||||
.l-palette-item {
|
||||
//@include display(flex);
|
||||
@include flex(1 0 auto);
|
||||
margin: 1px;
|
||||
min-width: auto;
|
||||
width: auto;
|
||||
&:before {
|
||||
content: '';
|
||||
padding-top: 75%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -80,23 +80,32 @@
|
||||
|
||||
// Editing Grids
|
||||
.l-grid-holder {
|
||||
display: block;
|
||||
.l-grid {
|
||||
&.l-grid-x { @include bgTicks($colorGridLines, 'x'); }
|
||||
&.l-grid-y { @include bgTicks($colorGridLines, 'y'); }
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent nested frames from showing their grids
|
||||
.t-frame-outer .l-grid-holder { display: none !important; }
|
||||
|
||||
// Prevent nested elements from showing s-hover-border
|
||||
.t-frame-outer .s-hover-border {
|
||||
border: none !important;
|
||||
// Display grid when selected or selection parent.
|
||||
.s-selected .l-grid-holder,
|
||||
.s-selected-parent .l-grid-holder {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Prevent nested frames from being selectable until we have proper sub-object editing
|
||||
.t-frame-outer .t-frame-outer {
|
||||
pointer-events: none;
|
||||
// Display in nested frames...
|
||||
.t-frame-outer {
|
||||
// ...when drilled in or selection parent...
|
||||
&.s-drilled-in, &.s-selected-parent {
|
||||
.l-grid-holder {
|
||||
display: block;
|
||||
}
|
||||
.t-frame-outer:not(.s-drilled-in) .l-grid-holder {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// ...but hide otherwise.
|
||||
.l-grid-holder {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,19 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
.section-header {
|
||||
border-radius: $basicCr;
|
||||
background: $colorFormSectionHeader;
|
||||
color: lighten($colorBodyFg, 20%);
|
||||
font-size: inherit;
|
||||
margin: $interiorMargin 0;
|
||||
padding: $formTBPad $formLRPad;
|
||||
text-transform: uppercase;
|
||||
.view-control {
|
||||
display: inline-block;
|
||||
margin-right: $interiorMargin;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
@ -41,15 +53,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
border-radius: $basicCr;
|
||||
background: $colorFormSectionHeader;
|
||||
$c: lighten($colorBodyFg, 20%);
|
||||
color: $c;
|
||||
font-size: 0.8em;
|
||||
padding: $formTBPad $formLRPad;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
$m: $interiorMargin;
|
||||
box-sizing: border-box;
|
||||
@ -57,9 +60,6 @@
|
||||
margin-bottom: $interiorMarginLg * 2;
|
||||
padding: $formTBPad 0;
|
||||
position: relative;
|
||||
//&ng-form {
|
||||
// display: block;
|
||||
//}
|
||||
|
||||
&.first {
|
||||
border-top: none;
|
||||
@ -171,3 +171,106 @@
|
||||
padding: $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
/**************************************************************************** COMPACT FORM */
|
||||
// ul > li > label, control
|
||||
// Make a new UL for each form section
|
||||
// Allow control-first, controls-below
|
||||
// TO-DO: migrate work in branch ch-plot-styling that users .inspector-config to use classes below instead
|
||||
|
||||
.l-compact-form .tree ul li,
|
||||
.l-compact-form ul li {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
|
||||
.l-compact-form {
|
||||
$labelW: 40%;
|
||||
$minW: $labelW;
|
||||
ul {
|
||||
margin-bottom: $interiorMarginLg;
|
||||
li {
|
||||
@include display(flex);
|
||||
@include flex-wrap(wrap);
|
||||
@include align-items(center);
|
||||
label,
|
||||
.control {
|
||||
@include display(flex);
|
||||
}
|
||||
label {
|
||||
line-height: inherit;
|
||||
width: $labelW;
|
||||
}
|
||||
.controls {
|
||||
@include flex-grow(1);
|
||||
margin-left: $interiorMargin;
|
||||
input[type="text"],
|
||||
input[type="search"],
|
||||
input[type="number"],
|
||||
.select {
|
||||
height: $btnStdH;
|
||||
line-height: $btnStdH;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.e-control {
|
||||
// Individual form controls
|
||||
&:not(:first-child) {
|
||||
margin-left: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.connects-to-previous {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
&.section-header {
|
||||
margin-top: $interiorMarginLg;
|
||||
border-top: 1px solid $colorFormLines;
|
||||
}
|
||||
|
||||
&.controls-first {
|
||||
.control {
|
||||
@include flex-grow(0);
|
||||
margin-right: $interiorMargin;
|
||||
min-width: 0;
|
||||
order: 1;
|
||||
width: auto;
|
||||
}
|
||||
label {
|
||||
@include flex-grow(1);
|
||||
order: 2;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
&.controls-under {
|
||||
display: block;
|
||||
.control, label {
|
||||
display: block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
ul li {
|
||||
border-top: none !important;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-error {
|
||||
// Block element that visually flags an error and contains a message
|
||||
background-color: $colorFormFieldErrorBg;
|
||||
color: $colorFormFieldErrorFg;
|
||||
border-radius: $basicCr;
|
||||
display: block;
|
||||
padding: 1px 6px;
|
||||
&:before {
|
||||
content: $glyph-icon-alert-triangle;
|
||||
display: inline;
|
||||
font-family: symbolsfont;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,18 +34,7 @@ body.touch {
|
||||
line-height: $mobileTreeItemH !important;
|
||||
.view-control {
|
||||
font-size: 1em;
|
||||
margin-right: $interiorMargin;
|
||||
width: ceil($mobileTreeItemH * 0.75);
|
||||
&.has-children {
|
||||
&:before {
|
||||
content: $glyph-icon-arrow-down;
|
||||
left: 50%;
|
||||
@include transform(translateX(-50%) rotate(-90deg));
|
||||
}
|
||||
&.expanded:before {
|
||||
@include transform(translateX(-50%) rotate(0deg));
|
||||
}
|
||||
}
|
||||
width: ceil($mobileTreeItemH * 0.5);
|
||||
}
|
||||
.t-object-label {
|
||||
line-height: inherit;
|
||||
|
@ -79,6 +79,7 @@
|
||||
|
||||
// Dialog boxes, size constrained and centered in desktop/tablet
|
||||
&.l-dialog {
|
||||
font-size: 0.8rem;
|
||||
.s-button {
|
||||
&:not(.major) {
|
||||
@include btnSubtle($bg: $colorOvrBtnBg, $bgHov: pullForward($colorOvrBtnBg, 10%), $fg: $colorOvrBtnFg, $fgHov: $colorOvrBtnFg, $ic: $colorOvrBtnFg, $icHov: $colorOvrBtnFg);
|
||||
@ -125,9 +126,9 @@
|
||||
@include containerSubtle($colorOvrBg, $colorOvrFg);
|
||||
}
|
||||
|
||||
.title {
|
||||
.dialog-title {
|
||||
@include ellipsize();
|
||||
font-size: 1.2em;
|
||||
font-size: 1.5em;
|
||||
line-height: 120%;
|
||||
margin-bottom: $interiorMargin;
|
||||
}
|
||||
|
208
platform/commonUI/general/res/sass/plots/_legend.scss
Normal file
208
platform/commonUI/general/res/sass/plots/_legend.scss
Normal file
@ -0,0 +1,208 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2017, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
.gl-plot {
|
||||
.gl-plot-legend {
|
||||
min-height: $plotLegendH;
|
||||
|
||||
.view-control {
|
||||
font-size: 1em;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
tr {
|
||||
display: table-row;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
@include ellipsize(); // Note: this won't work if table-layout uses anything other than fixed.
|
||||
display: table-cell;
|
||||
padding: 1px 3px; // Tighter than standard tabular padding
|
||||
}
|
||||
}
|
||||
|
||||
&.hover-on-plot {
|
||||
// User is hovering over the plot to get a value at a point
|
||||
.hover-value-enabled {
|
||||
background-color: $legendHoverValueBg;
|
||||
border-radius: $smallCr;
|
||||
padding: 0 $interiorMarginSm;
|
||||
&:before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
&.cursor-hover,
|
||||
.value-to-display-nearestTimestamp,
|
||||
.value-to-display-nearestValue
|
||||
{
|
||||
@extend .icon-crosshair-12px;
|
||||
&:before {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
&.value-to-display-min:before {
|
||||
content: 'MIN ';
|
||||
}
|
||||
&.value-to-display-max:before {
|
||||
content: 'MAX ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.plot-legend-collapsed .plot-wrapper-expanded-legend { display: none; }
|
||||
&.plot-legend-expanded .plot-wrapper-collapsed-legend { display: none; }
|
||||
|
||||
/***************** GENERAL STYLES, ALL STATES */
|
||||
.plot-legend-item {
|
||||
// General styles for legend items, both expanded and collapsed legend states
|
||||
.plot-series-color-swatch {
|
||||
border-radius: $smallCr;
|
||||
border: 1px solid $colorBodyBg;
|
||||
display: inline-block;
|
||||
height: $plotSwatchD;
|
||||
width: $plotSwatchD;
|
||||
}
|
||||
.plot-series-name {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.plot-series-value {
|
||||
@include ellipsize();
|
||||
}
|
||||
}
|
||||
|
||||
/***************** GENERAL STYLES, COLLAPSED */
|
||||
&.plot-legend-collapsed {
|
||||
// .plot-legend-item is a span of spans.
|
||||
&.plot-legend-top .gl-plot-legend { margin-bottom: $interiorMargin; }
|
||||
&.plot-legend-bottom .gl-plot-legend { margin-top: $interiorMargin; }
|
||||
&.plot-legend-right .gl-plot-legend { margin-left: $interiorMargin; }
|
||||
&.plot-legend-left .gl-plot-legend { margin-right: $interiorMargin; }
|
||||
|
||||
.plot-legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&:not(:first-child) {
|
||||
margin-left: $interiorMarginLg;
|
||||
}
|
||||
.plot-series-swatch-and-name,
|
||||
.plot-series-value {
|
||||
@include ellipsize();
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.plot-series-swatch-and-name {
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
|
||||
.plot-series-value {
|
||||
text-align: left;
|
||||
width: 170px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***************** GENERAL STYLES, EXPANDED */
|
||||
&.plot-legend-expanded {
|
||||
.gl-plot-legend {
|
||||
max-height: 70%;
|
||||
}
|
||||
|
||||
.plot-wrapper-expanded-legend {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&.plot-legend-top .gl-plot-legend {
|
||||
margin-bottom: $interiorMargin;
|
||||
}
|
||||
&.plot-legend-bottom .gl-plot-legend {
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
/***************** TOP OR BOTTOM */
|
||||
&.plot-legend-top,
|
||||
&.plot-legend-bottom {
|
||||
// General styles when legend is on the top or bottom
|
||||
@extend .l-flex-col;
|
||||
&.plot-legend-collapsed {
|
||||
// COLLAPSED ON TOP OR BOTTOM
|
||||
.plot-wrapper-collapsed-legend {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***************** EITHER SIDE */
|
||||
&.plot-legend-left,
|
||||
&.plot-legend-right {
|
||||
@extend .l-flex-row;
|
||||
// If the legend is expanded, use flex-col instead so that the legend gets the width it needs.
|
||||
&.plot-legend-expanded {
|
||||
// EXPANDED, ON EITHER SIDE
|
||||
@extend .l-flex-col;
|
||||
}
|
||||
|
||||
&.plot-legend-collapsed {
|
||||
// COLLAPSED, ON EITHER SIDE
|
||||
.gl-plot-legend {
|
||||
max-height: inherit;
|
||||
width: 25%;
|
||||
}
|
||||
.plot-wrapper-collapsed-legend {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.plot-legend-item {
|
||||
margin-bottom: 1px;
|
||||
margin-left: 0;
|
||||
flex-wrap: wrap;
|
||||
.plot-series-swatch-and-name {
|
||||
flex: 0 1 auto;
|
||||
min-width: 20%;
|
||||
}
|
||||
.plot-series-value {
|
||||
flex: 0 1 auto;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***************** ON BOTTOM OR RIGHT */
|
||||
&.plot-legend-right:not(.plot-legend-expanded),
|
||||
&.plot-legend-bottom {
|
||||
.gl-plot-legend {
|
||||
order: 2;
|
||||
}
|
||||
.plot-wrapper-axis-and-display-area {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
}
|
@ -20,18 +20,64 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
.abs.holder-plot {
|
||||
// Fend off the scrollbar when less than min-height;
|
||||
right: $interiorMargin;
|
||||
right: $interiorMargin; // Fend off the scrollbar when less than min-height;
|
||||
.t-object-alert.t-alert-unsynced {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/********************************************* STACKED PLOT LAYOUT */
|
||||
.t-plot-stacked {
|
||||
.l-view-section {
|
||||
// Make this a flex container
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
.gl-plot.child-frame {
|
||||
mct-plot {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
flex: 1 1 auto;
|
||||
&:not(:first-child) {
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.s-status-timeconductor-unsynced .holder-plot {
|
||||
.t-object-alert.t-alert-unsynced {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.gl-plot {
|
||||
color: $colorPlotFg;
|
||||
display: flex;
|
||||
font-size: 0.7rem;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: $plotMinH;
|
||||
|
||||
/********************************************* AXIS AND DISPLAY AREA */
|
||||
.plot-wrapper-axis-and-display-area {
|
||||
margin-top: $interiorMargin; // Keep the top tick label from getting clipped
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
.t-object-alert {
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-size: 1.5em;
|
||||
top: $interiorMarginSm; left: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
.gl-plot-wrapper-display-area-and-x-axis {
|
||||
// Holds the plot area and the X-axis only
|
||||
position: absolute;
|
||||
@ -49,7 +95,6 @@
|
||||
}
|
||||
|
||||
.gl-plot-axis-area.gl-plot-x {
|
||||
//@include test(green);
|
||||
top: auto;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
@ -63,7 +108,7 @@
|
||||
.gl-plot-axis-area {
|
||||
position: absolute;
|
||||
&.gl-plot-y {
|
||||
top: $plotLegendH + $interiorMargin;
|
||||
top: nth($plotDisplayArea, 1);
|
||||
right: auto;
|
||||
bottom: nth($plotDisplayArea, 3);
|
||||
left: 0;
|
||||
@ -158,17 +203,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.gl-plot-legend {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: auto;
|
||||
left: 0;
|
||||
height: $plotLegendH;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/****************************** Limits and Out-of-Bounds data */
|
||||
|
||||
.l-limit-bar,
|
||||
@ -235,39 +269,6 @@
|
||||
border: 1px solid $colorPlotAreaBorder;
|
||||
}
|
||||
|
||||
.gl-plot-legend,
|
||||
.legend {
|
||||
.plot-legend-item,
|
||||
.legend-item {
|
||||
display: inline-block;
|
||||
margin-right: $interiorMarginLg;
|
||||
margin-bottom: $interiorMarginSm;
|
||||
span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.plot-color-swatch,
|
||||
.color-swatch {
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
height: $plotSwatchD;
|
||||
width: $plotSwatchD;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gl-plot-legend {
|
||||
.plot-legend-item {
|
||||
border-radius: $smallCr;
|
||||
line-height: 1.5em;
|
||||
padding: 0px $itemPadLR;
|
||||
.plot-color-swatch {
|
||||
border: 1px solid $colorBodyBg;
|
||||
height: $plotSwatchD + 1;
|
||||
width: $plotSwatchD + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tick {
|
||||
position: absolute;
|
||||
border: 0 $colorPlotHash solid;
|
||||
|
@ -23,7 +23,7 @@
|
||||
ul.tree {
|
||||
@include menuUlReset();
|
||||
@include user-select(none);
|
||||
li {
|
||||
> li {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
@ -52,21 +52,11 @@ ul.tree {
|
||||
|
||||
.view-control {
|
||||
color: $colorItemTreeVC;
|
||||
font-size: 0.75em;
|
||||
margin-right: $interiorMargin;
|
||||
height: 100%;
|
||||
line-height: inherit;
|
||||
width: $treeVCW;
|
||||
&.has-children {
|
||||
&:before {
|
||||
position: absolute;
|
||||
@include trans-prop-nice(transform, 100ms);
|
||||
content: "\e904";
|
||||
@include transform-origin(center);
|
||||
}
|
||||
&.expanded:before {
|
||||
@include transform(rotate(90deg));
|
||||
}
|
||||
&:before { display: block; }
|
||||
&.no-children {
|
||||
&:before { display: none; }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,12 +26,10 @@
|
||||
z-index: 0; // Needed to prevent child-frame controls from showing through when another child-frame is above
|
||||
&:not(.no-frame) {
|
||||
background: $colorBodyBg;
|
||||
border: 1px solid $bc;
|
||||
&:hover {
|
||||
border-color: lighten($bc, 10%);
|
||||
}
|
||||
border-color: $bc;
|
||||
}
|
||||
}
|
||||
|
||||
.object-browse-bar {
|
||||
font-size: 0.75em;
|
||||
height: $ohH;
|
||||
@ -44,7 +42,8 @@
|
||||
|
||||
&.t-object-type-timer,
|
||||
&.t-object-type-clock,
|
||||
&.t-object-type-hyperlink {
|
||||
&.t-object-type-hyperlink,
|
||||
&.t-object-type-summary-widget {
|
||||
// Hide the right side buttons for objects where they don't make sense
|
||||
// Note that this will hide the view Switcher button if applied
|
||||
// to an object that has it.
|
||||
@ -91,9 +90,9 @@
|
||||
|
||||
&.no-frame {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
border-color: transparent;
|
||||
.object-browse-bar .right {
|
||||
$m: 0; // $interiorMarginSm;
|
||||
$m: 0;
|
||||
background: rgba(black, 0.3);
|
||||
border-radius: $basicCr;
|
||||
padding: $interiorMarginSm;
|
||||
@ -103,7 +102,7 @@
|
||||
}
|
||||
&.t-frame-outer > .t-rep-frame {
|
||||
&.contents {
|
||||
$m: 2px;
|
||||
$m: 0px;
|
||||
top: $m;
|
||||
right: $m;
|
||||
bottom: $m;
|
||||
@ -114,6 +113,7 @@
|
||||
display: none;
|
||||
}
|
||||
> .object-holder.abs {
|
||||
overflow: hidden;
|
||||
top: 0 !important;
|
||||
}
|
||||
}
|
||||
@ -125,14 +125,21 @@
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/********************************************************** OBJECT TYPES */
|
||||
.t-object-type-hyperlink {
|
||||
/********************************************************** OBJECT TYPES */
|
||||
.t-object-type-hyperlink,
|
||||
.t-object-type-summary-widget {
|
||||
.object-holder {
|
||||
overflow: hidden;
|
||||
}
|
||||
.w-summary-widget,
|
||||
.l-summary-widget,
|
||||
.l-hyperlink.s-button {
|
||||
// When a hyperlink is a button in a frame, make it expand to fill out to the object-holder
|
||||
// Some object types expand to the full size of the object-holder.
|
||||
@extend .abs;
|
||||
}
|
||||
|
||||
.l-summary-widget,
|
||||
.l-hyperlink.s-button {
|
||||
.label {
|
||||
@include ellipsize();
|
||||
@include transform(translateY(-50%));
|
||||
|
@ -20,35 +20,52 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
.s-hover-border {
|
||||
border: 1px dotted transparent;
|
||||
border: 1px solid transparent;
|
||||
&:hover {
|
||||
border-color: rgba($colorSelectableSelectedPrimary, 0.5) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.s-status-editing {
|
||||
// Limit to editing mode until we have sub-object selection
|
||||
// Limit to editing mode
|
||||
$o: 0.5;
|
||||
$oHover: 0.8;
|
||||
$bc: $colorSelectableSelectedPrimary;
|
||||
.s-hover-border {
|
||||
// Show a border by default so user can see object bounds and empty objects
|
||||
border: 1px dotted rgba($colorSelectableSelectedPrimary, 0.3) !important;
|
||||
border-color: rgba($bc, $o) !important;
|
||||
border-style: dotted !important;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba($colorSelectableSelectedPrimary, 0.7) !important;
|
||||
border-color: rgba($bc, $oHover) !important;
|
||||
}
|
||||
|
||||
&.t-object-type-layout {
|
||||
border-style: dashed !important;
|
||||
}
|
||||
}
|
||||
|
||||
.s-selected > .s-hover-border,
|
||||
.s-selected.s-hover-border {
|
||||
// Styles for a selected object. Also used by legacy Fixed Position/Panel objects.
|
||||
border-color: $colorSelectableSelectedPrimary !important;
|
||||
@include boxShdwLarge();
|
||||
// Show edit-corners if you got 'em
|
||||
.edit-corner {
|
||||
display: block;
|
||||
&:hover {
|
||||
background-color: rgba($colorKey, 1);
|
||||
.s-selected {
|
||||
&.s-moveable {
|
||||
&:not(.s-drilled-in) {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.s-selected > .s-moveable,
|
||||
.s-selected.s-moveable {
|
||||
cursor: move;
|
||||
.s-selected > .s-hover-border,
|
||||
.s-selected.s-hover-border {
|
||||
// Styles for a selected object. Also used by legacy Fixed Position/Panel objects.
|
||||
border-color: $colorSelectableSelectedPrimary !important;
|
||||
@include boxShdwLarge();
|
||||
// Show edit-corners if you got 'em
|
||||
.edit-corner {
|
||||
display: block;
|
||||
&:hover {
|
||||
background-color: rgba($colorKey, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<span ng-controller="DateTimeFieldController">
|
||||
<input type="text"
|
||||
<input type="text" autocorrect="off" spellcheck="false"
|
||||
ng-model="textValue"
|
||||
ng-blur="restoreTextValue(); ngBlur()"
|
||||
ng-mouseup="ngMouseup()"
|
||||
|
@ -40,7 +40,7 @@ define(
|
||||
|
||||
// Gets an array of the contextual parents/ancestors of the selected object
|
||||
function getContextualPath() {
|
||||
var currentObj = $scope.ngModel.selectedObject,
|
||||
var currentObj = $scope.domainObject,
|
||||
currentParent,
|
||||
parents = [];
|
||||
|
||||
@ -68,7 +68,7 @@ define(
|
||||
|
||||
// If this the the initial call of this recursive function
|
||||
if (!current) {
|
||||
current = $scope.ngModel.selectedObject;
|
||||
current = $scope.domainObject;
|
||||
$scope.primaryParents = [];
|
||||
}
|
||||
|
||||
@ -87,16 +87,16 @@ define(
|
||||
|
||||
// Gets the metadata for the selected object
|
||||
function getMetadata() {
|
||||
$scope.metadata = $scope.ngModel.selectedObject &&
|
||||
$scope.ngModel.selectedObject.hasCapability('metadata') &&
|
||||
$scope.ngModel.selectedObject.useCapability('metadata');
|
||||
$scope.metadata = $scope.domainObject &&
|
||||
$scope.domainObject.hasCapability('metadata') &&
|
||||
$scope.domainObject.useCapability('metadata');
|
||||
}
|
||||
|
||||
// Set scope variables when the selected object changes
|
||||
$scope.$watch('ngModel.selectedObject', function () {
|
||||
$scope.isLink = $scope.ngModel.selectedObject &&
|
||||
$scope.ngModel.selectedObject.hasCapability('location') &&
|
||||
$scope.ngModel.selectedObject.getCapability('location').isLink();
|
||||
$scope.$watch('domainObject', function () {
|
||||
$scope.isLink = $scope.domainObject &&
|
||||
$scope.domainObject.hasCapability('location') &&
|
||||
$scope.domainObject.getCapability('location').isLink();
|
||||
|
||||
if ($scope.isLink) {
|
||||
getPrimaryPath();
|
||||
@ -109,7 +109,7 @@ define(
|
||||
getMetadata();
|
||||
});
|
||||
|
||||
var mutation = $scope.ngModel.selectedObject.getCapability('mutation');
|
||||
var mutation = $scope.domainObject.getCapability('mutation');
|
||||
var unlisten = mutation.listen(getMetadata);
|
||||
$scope.$on('$destroy', unlisten);
|
||||
}
|
||||
|
@ -21,39 +21,40 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["./SubPlot"],
|
||||
function (SubPlot) {
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* Utility factory; wraps the SubPlot constructor and adds
|
||||
* in a reference to the telemetryFormatter, which will be
|
||||
* used to represent telemetry values (timestamps or data
|
||||
* values) as human-readable strings.
|
||||
* @memberof platform/features/plot
|
||||
* The mct-selectable directive allows selection functionality
|
||||
* (click) to be attached to specific elements.
|
||||
*
|
||||
* @memberof platform/commonUI/general
|
||||
* @constructor
|
||||
*/
|
||||
function SubPlotFactory(telemetryFormatter) {
|
||||
this.telemetryFormatter = telemetryFormatter;
|
||||
function MCTSelectable(openmct) {
|
||||
|
||||
// Link; install event handlers.
|
||||
function link(scope, element, attrs) {
|
||||
var removeSelectable = openmct.selection.selectable(
|
||||
element[0],
|
||||
scope.$eval(attrs.mctSelectable),
|
||||
attrs.hasOwnProperty('mctInitSelect') && scope.$eval(attrs.mctInitSelect) !== false
|
||||
);
|
||||
|
||||
scope.$on("$destroy", function () {
|
||||
removeSelectable();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
// mct-selectable only makes sense as an attribute
|
||||
restrict: "A",
|
||||
// Link function, to install event handlers
|
||||
link: link
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new sub-plot.
|
||||
* @param {DomainObject[]} telemetryObjects the domain objects
|
||||
* which will be plotted in this sub-plot
|
||||
* @param {PlotPanZoomStack} panZoomStack the stack of pan-zoom
|
||||
* states which is applicable to this sub-plot
|
||||
* @returns {SubPlot} the instantiated sub-plot
|
||||
* @method
|
||||
*/
|
||||
SubPlotFactory.prototype.createSubPlot = function (telemetryObjects, panZoomStack) {
|
||||
return new SubPlot(
|
||||
telemetryObjects,
|
||||
panZoomStack,
|
||||
this.telemetryFormatter
|
||||
);
|
||||
};
|
||||
|
||||
return SubPlotFactory;
|
||||
|
||||
return MCTSelectable;
|
||||
}
|
||||
);
|
@ -83,9 +83,9 @@ define([
|
||||
this.activeObject = domainObject;
|
||||
|
||||
if (domainObject && domainObject.hasCapability('composition')) {
|
||||
$(this.toggleView.elements()).addClass('has-children');
|
||||
$(this.toggleView.elements()).removeClass('no-children');
|
||||
} else {
|
||||
$(this.toggleView.elements()).removeClass('has-children');
|
||||
$(this.toggleView.elements()).addClass('no-children');
|
||||
}
|
||||
|
||||
if (domainObject && domainObject.hasCapability('status')) {
|
||||
|
@ -41,16 +41,6 @@ define(
|
||||
"$scope",
|
||||
["$watch", "$on"]
|
||||
);
|
||||
mockScope.ngModel = {};
|
||||
mockScope.ngModel.selectedObject = {
|
||||
getCapability: function () {
|
||||
return {
|
||||
listen: function () {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
mockObjectService = jasmine.createSpyObj(
|
||||
"objectService",
|
||||
@ -77,22 +67,27 @@ define(
|
||||
"location capability",
|
||||
["isLink"]
|
||||
);
|
||||
|
||||
mockDomainObject.getCapability.andCallFake(function (param) {
|
||||
if (param === 'location') {
|
||||
return mockLocationCapability;
|
||||
} else if (param === 'context') {
|
||||
return mockContextCapability;
|
||||
} else if (param === 'mutation') {
|
||||
return {
|
||||
listen: function () {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
controller = new ObjectInspectorController(mockScope, mockObjectService);
|
||||
|
||||
// Change the selected object to trigger the watch call
|
||||
mockScope.ngModel.selectedObject = mockDomainObject;
|
||||
});
|
||||
|
||||
it("watches for changes to the selected object", function () {
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith('ngModel.selectedObject', jasmine.any(Function));
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith('domainObject', jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("looks for contextual parent objects", function () {
|
||||
|
@ -65,6 +65,10 @@ define(
|
||||
options = Object.create(OPTIONS);
|
||||
options.marginX = -bubbleSpaceLR;
|
||||
|
||||
// prevent bubble from appearing right under pointer,
|
||||
// which causes hover callback to be called multiple times
|
||||
options.offsetX = 1;
|
||||
|
||||
// On a phone, bubble takes up more screen real estate,
|
||||
// so position it differently (toward the bottom)
|
||||
if (this.agentService.isPhone()) {
|
||||
|
@ -38,7 +38,8 @@ define([
|
||||
"implementation": InspectorController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"policyService"
|
||||
"openmct",
|
||||
"$document"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
@ -21,44 +21,73 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
['../../browse/src/InspectorRegion'],
|
||||
function (InspectorRegion) {
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* The InspectorController adds region data for a domain object's type
|
||||
* to the scope.
|
||||
* The InspectorController listens for the selection changes and adds the selection
|
||||
* object to the scope.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function InspectorController($scope, policyService) {
|
||||
var domainObject = $scope.domainObject,
|
||||
typeCapability = domainObject.getCapability('type'),
|
||||
statusListener;
|
||||
function InspectorController($scope, openmct, $document) {
|
||||
var self = this;
|
||||
self.$scope = $scope;
|
||||
|
||||
/**
|
||||
* Filters region parts to only those allowed by region policies
|
||||
* @param regions
|
||||
* @returns {{}}
|
||||
* Callback handler for the selection change event.
|
||||
* Adds the selection object to the scope. If the selected item has an inspector view,
|
||||
* it puts the key in the scope. If provider view exists, it shows the view.
|
||||
*/
|
||||
function filterRegions(inspector) {
|
||||
//Dupe so we're not modifying the type definition.
|
||||
return inspector.regions && inspector.regions.filter(function (region) {
|
||||
return policyService.allow('region', region, domainObject);
|
||||
});
|
||||
function setSelection(selection) {
|
||||
if (selection[0]) {
|
||||
var view = openmct.inspectorViews.get(selection);
|
||||
var container = $document[0].querySelectorAll('.inspector-provider-view')[0];
|
||||
container.innerHTML = "";
|
||||
|
||||
if (view) {
|
||||
self.providerView = true;
|
||||
view.show(container);
|
||||
} else {
|
||||
self.providerView = false;
|
||||
var selectedItem = selection[0].context.oldItem;
|
||||
|
||||
if (selectedItem) {
|
||||
$scope.inspectorKey = selectedItem.getCapability("type").typeDef.inspector;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.$scope.selection = selection;
|
||||
}
|
||||
|
||||
function setRegions() {
|
||||
$scope.regions = filterRegions(typeCapability.getDefinition().inspector || new InspectorRegion());
|
||||
}
|
||||
openmct.selection.on("change", setSelection);
|
||||
|
||||
setSelection(openmct.selection.get());
|
||||
|
||||
statusListener = domainObject.getCapability("status").listen(setRegions);
|
||||
$scope.$on("$destroy", function () {
|
||||
statusListener();
|
||||
openmct.selection.off("change", setSelection);
|
||||
});
|
||||
|
||||
setRegions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the selected item.
|
||||
*
|
||||
* @returns a domain object
|
||||
*/
|
||||
InspectorController.prototype.selectedItem = function () {
|
||||
return this.$scope.selection[0].context.oldItem;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a provider view exists.
|
||||
*
|
||||
* @returns 'true' if provider view exists, 'false' otherwise
|
||||
*/
|
||||
InspectorController.prototype.hasProviderView = function () {
|
||||
return this.providerView;
|
||||
};
|
||||
|
||||
return InspectorController;
|
||||
}
|
||||
);
|
||||
|
@ -27,82 +27,93 @@ define(
|
||||
describe("The inspector controller ", function () {
|
||||
var mockScope,
|
||||
mockDomainObject,
|
||||
mockTypeCapability,
|
||||
mockTypeDefinition,
|
||||
mockPolicyService,
|
||||
mockStatusCapability,
|
||||
capabilities = {},
|
||||
controller;
|
||||
mockOpenMCT,
|
||||
mockSelection,
|
||||
mockInspectorViews,
|
||||
mockTypeDef,
|
||||
controller,
|
||||
container,
|
||||
$document = [],
|
||||
selectable = [];
|
||||
|
||||
beforeEach(function () {
|
||||
mockTypeDefinition = {
|
||||
inspector:
|
||||
{
|
||||
'regions': [
|
||||
{'name': 'Part One'},
|
||||
{'name': 'Part Two'}
|
||||
]
|
||||
}
|
||||
mockTypeDef = {
|
||||
typeDef: {
|
||||
inspector: "some-key"
|
||||
}
|
||||
};
|
||||
|
||||
mockTypeCapability = jasmine.createSpyObj('typeCapability', [
|
||||
'getDefinition'
|
||||
]);
|
||||
mockTypeCapability.getDefinition.andReturn(mockTypeDefinition);
|
||||
capabilities.type = mockTypeCapability;
|
||||
|
||||
mockStatusCapability = jasmine.createSpyObj('statusCapability', [
|
||||
'listen'
|
||||
]);
|
||||
capabilities.status = mockStatusCapability;
|
||||
|
||||
mockDomainObject = jasmine.createSpyObj('domainObject', [
|
||||
'getCapability'
|
||||
]);
|
||||
mockDomainObject.getCapability.andCallFake(function (name) {
|
||||
return capabilities[name];
|
||||
});
|
||||
|
||||
mockPolicyService = jasmine.createSpyObj('policyService', [
|
||||
'allow'
|
||||
]);
|
||||
mockDomainObject.getCapability.andReturn(mockTypeDef);
|
||||
|
||||
mockScope = jasmine.createSpyObj('$scope',
|
||||
['$on']
|
||||
['$on', 'selection']
|
||||
);
|
||||
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
selectable[0] = {
|
||||
context: {
|
||||
oldItem: mockDomainObject
|
||||
}
|
||||
};
|
||||
|
||||
mockSelection = jasmine.createSpyObj("selection", [
|
||||
'on',
|
||||
'off',
|
||||
'get'
|
||||
]);
|
||||
mockSelection.get.andReturn(selectable);
|
||||
|
||||
mockInspectorViews = jasmine.createSpyObj('inspectorViews', ['get']);
|
||||
mockOpenMCT = {
|
||||
selection: mockSelection,
|
||||
inspectorViews: mockInspectorViews
|
||||
};
|
||||
|
||||
container = jasmine.createSpy('container', ['innerHTML']);
|
||||
$document[0] = jasmine.createSpyObj("$document", ['querySelectorAll']);
|
||||
$document[0].querySelectorAll.andReturn([container]);
|
||||
|
||||
controller = new InspectorController(mockScope, mockOpenMCT, $document);
|
||||
});
|
||||
|
||||
it("filters out regions disallowed by region policy", function () {
|
||||
mockPolicyService.allow.andReturn(false);
|
||||
controller = new InspectorController(mockScope, mockPolicyService);
|
||||
expect(mockScope.regions.length).toBe(0);
|
||||
it("listens for selection change event", function () {
|
||||
expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
|
||||
'change',
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
expect(controller.selectedItem()).toEqual(mockDomainObject);
|
||||
|
||||
var mockItem = jasmine.createSpyObj('domainObject', [
|
||||
'getCapability'
|
||||
]);
|
||||
mockItem.getCapability.andReturn(mockTypeDef);
|
||||
selectable[0].context.oldItem = mockItem;
|
||||
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(controller.selectedItem()).toEqual(mockItem);
|
||||
});
|
||||
|
||||
it("does not filter out regions allowed by region policy", function () {
|
||||
mockPolicyService.allow.andReturn(true);
|
||||
controller = new InspectorController(mockScope, mockPolicyService);
|
||||
expect(mockScope.regions.length).toBe(2);
|
||||
it("cleans up on scope destroy", function () {
|
||||
expect(mockScope.$on).toHaveBeenCalledWith(
|
||||
'$destroy',
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
mockScope.$on.calls[0].args[1]();
|
||||
|
||||
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
|
||||
'change',
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("Responds to status changes", function () {
|
||||
mockPolicyService.allow.andReturn(true);
|
||||
controller = new InspectorController(mockScope, mockPolicyService);
|
||||
expect(mockScope.regions.length).toBe(2);
|
||||
expect(mockStatusCapability.listen).toHaveBeenCalled();
|
||||
mockPolicyService.allow.andReturn(false);
|
||||
mockStatusCapability.listen.mostRecentCall.args[0]();
|
||||
expect(mockScope.regions.length).toBe(0);
|
||||
});
|
||||
|
||||
it("Unregisters status listener", function () {
|
||||
var mockListener = jasmine.createSpy('listener');
|
||||
mockStatusCapability.listen.andReturn(mockListener);
|
||||
controller = new InspectorController(mockScope, mockPolicyService);
|
||||
expect(mockScope.$on).toHaveBeenCalledWith("$destroy", jasmine.any(Function));
|
||||
mockScope.$on.mostRecentCall.args[1]();
|
||||
expect(mockListener).toHaveBeenCalled();
|
||||
it("adds selection object to scope", function () {
|
||||
expect(mockScope.selection).toEqual(selectable);
|
||||
expect(controller.selectedItem()).toEqual(mockDomainObject);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -181,6 +181,8 @@ $colorPlotHash: $colorTick;
|
||||
$stylePlotHash: dashed;
|
||||
$colorPlotAreaBorder: $colorInteriorBorder;
|
||||
$colorPlotLabelFg: pushBack($colorPlotFg, 20%);
|
||||
$legendCollapsedNameMaxW: 50%;
|
||||
$legendHoverValueBg: rgba($colorBodyFg, 0.1);
|
||||
|
||||
// Tree
|
||||
$colorItemTreeHoverBg: pullForward($colorBodyBg, $hoverRatioPercent);
|
||||
@ -243,6 +245,12 @@ $colorCalCellSelectedBg: $colorItemTreeSelectedBg;
|
||||
$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
|
||||
$colorCalCellInMonthBg: pushBack($colorMenuBg, 5%);
|
||||
|
||||
// Palettes
|
||||
$colorPaletteFg: pullForward($colorMenuBg, 30%);
|
||||
$colorPaletteSelected: #fff;
|
||||
$shdwPaletteFg: black 0 0 2px;
|
||||
$shdwPaletteSelected: inset 0 0 0 1px #000;
|
||||
|
||||
// About Screen
|
||||
$colorAboutLink: #84b3ff;
|
||||
|
||||
|
@ -181,6 +181,8 @@ $colorPlotHash: $colorTick;
|
||||
$stylePlotHash: dashed;
|
||||
$colorPlotAreaBorder: $colorInteriorBorder;
|
||||
$colorPlotLabelFg: pushBack($colorPlotFg, 20%);
|
||||
$legendCollapsedNameMaxW: 50%;
|
||||
$legendHoverValueBg: rgba($colorBodyFg, 0.2);
|
||||
|
||||
// Tree
|
||||
$colorItemTreeHoverBg: pullForward($colorBodyBg, $hoverRatioPercent);
|
||||
@ -243,6 +245,12 @@ $colorCalCellSelectedBg: $colorItemTreeSelectedBg;
|
||||
$colorCalCellSelectedFg: $colorItemTreeSelectedFg;
|
||||
$colorCalCellInMonthBg: pullForward($colorMenuBg, 5%);
|
||||
|
||||
// Palettes
|
||||
$colorPaletteFg: pullForward($colorMenuBg, 30%);
|
||||
$colorPaletteSelected: #333;
|
||||
$shdwPaletteFg: none;
|
||||
$shdwPaletteSelected: inset 0 0 0 1px #fff;
|
||||
|
||||
// About Screen
|
||||
$colorAboutLink: #84b3ff;
|
||||
|
||||
|
@ -1,54 +0,0 @@
|
||||
define([
|
||||
'text!./res/templates/autoflow-tabular.html',
|
||||
'./src/AutoflowTabularController',
|
||||
'./src/MCTAutoflowTable'
|
||||
], function (
|
||||
autoflowTabularTemplate,
|
||||
AutoflowTabularController,
|
||||
MCTAutoflowTable
|
||||
) {
|
||||
return function (options) {
|
||||
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,
|
||||
"type": options && options.type,
|
||||
"needs": [
|
||||
"telemetry"
|
||||
],
|
||||
"delegation": true
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
{
|
||||
"key": "AutoflowTabularController",
|
||||
"implementation": AutoflowTabularController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"$timeout",
|
||||
"telemetrySubscriber"
|
||||
]
|
||||
}
|
||||
],
|
||||
"directives": [
|
||||
{
|
||||
"key": "mctAutoflowTable",
|
||||
"implementation": MCTAutoflowTable
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
openmct.legacyRegistry.enable("platform/features/autoflow");
|
||||
};
|
||||
};
|
||||
});
|
||||
|
@ -1,26 +0,0 @@
|
||||
<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>
|
@ -1,169 +0,0 @@
|
||||
/*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;
|
||||
}
|
||||
);
|
@ -1,324 +0,0 @@
|
||||
|
||||
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;
|
||||
}
|
||||
);
|
@ -1,60 +0,0 @@
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
);
|
@ -1,178 +0,0 @@
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -1,341 +0,0 @@
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -1,39 +0,0 @@
|
||||
|
||||
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));
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
@ -45,7 +45,7 @@ define(
|
||||
|
||||
FollowIndicator.prototype.getText = function () {
|
||||
var timer = this.timerService.getTimer();
|
||||
return (timer) ? 'Following timer ' + timer.getModel().name : NO_TIMER;
|
||||
return timer ? ('Following timer ' + timer.name) : NO_TIMER;
|
||||
};
|
||||
|
||||
FollowIndicator.prototype.getDescription = function () {
|
||||
|
@ -42,18 +42,15 @@ define(["../../src/indicators/FollowIndicator"], function (FollowIndicator) {
|
||||
});
|
||||
|
||||
describe("when a timer is set", function () {
|
||||
var testModel;
|
||||
var mockDomainObject;
|
||||
var testObject;
|
||||
|
||||
beforeEach(function () {
|
||||
testModel = { name: "some timer!" };
|
||||
mockDomainObject = jasmine.createSpyObj('timer', ['getModel']);
|
||||
mockDomainObject.getModel.andReturn(testModel);
|
||||
mockTimerService.getTimer.andReturn(mockDomainObject);
|
||||
testObject = { name: "some timer!" };
|
||||
mockTimerService.getTimer.andReturn(testObject);
|
||||
});
|
||||
|
||||
it("displays the timer's name", function () {
|
||||
expect(indicator.getText().indexOf(testModel.name))
|
||||
expect(indicator.getText().indexOf(testObject.name))
|
||||
.not.toEqual(-1);
|
||||
});
|
||||
});
|
||||
|
@ -255,6 +255,8 @@ define(
|
||||
if (this.nextDatum) {
|
||||
this.updateValues(this.nextDatum);
|
||||
delete this.nextDatum;
|
||||
} else {
|
||||
this.updateValues(this.$scope.imageHistory[this.$scope.imageHistory.length - 1]);
|
||||
}
|
||||
this.autoScroll = true;
|
||||
}
|
||||
|
@ -183,6 +183,17 @@ define(
|
||||
expect(controller.getImageUrl()).toEqual(newUrl);
|
||||
});
|
||||
|
||||
it("forwards large image view to latest image in history on un-pause", function () {
|
||||
$scope.imageHistory = [
|
||||
{ utc: 1434600258122, url: 'some/url1', selected: false},
|
||||
{ utc: 1434600258123, url: 'some/url2', selected: false}
|
||||
];
|
||||
controller.paused(true);
|
||||
controller.paused(false);
|
||||
|
||||
expect(controller.getImageUrl()).toEqual(controller.getImageUrl($scope.imageHistory[1]));
|
||||
});
|
||||
|
||||
it("subscribes to telemetry", function () {
|
||||
expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(
|
||||
newDomainObject,
|
||||
@ -227,7 +238,7 @@ define(
|
||||
expect(controller.updateHistory(mockDatum)).toBe(false);
|
||||
});
|
||||
|
||||
describe("user clicks on imagery thumbnail", function () {
|
||||
describe("when user clicks on imagery thumbnail", function () {
|
||||
var mockDatum = { utc: 1434600258123, url: 'some/url', selected: false};
|
||||
|
||||
it("pauses and adds selected class to imagery thumbnail", function () {
|
||||
@ -248,6 +259,7 @@ define(
|
||||
expect(controller.getTime()).toEqual(controller.timeFormat.format(mockDatum.utc));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("initially shows an empty string for date/time", function () {
|
||||
|
@ -260,7 +260,9 @@ define([
|
||||
"key": "LayoutController",
|
||||
"implementation": LayoutController,
|
||||
"depends": [
|
||||
"$scope"
|
||||
"$scope",
|
||||
"$element",
|
||||
"openmct"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -270,7 +272,8 @@ define([
|
||||
"$scope",
|
||||
"$q",
|
||||
"dialogService",
|
||||
"openmct"
|
||||
"openmct",
|
||||
"$element"
|
||||
]
|
||||
}
|
||||
],
|
||||
@ -334,46 +337,6 @@ define([
|
||||
"conversion": "number[]"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "telemetry.panel",
|
||||
"name": "Telemetry Panel",
|
||||
"cssClass": "icon-telemetry-panel",
|
||||
"description": "A panel for collecting telemetry elements.",
|
||||
"priority": 899,
|
||||
"delegates": [
|
||||
"telemetry"
|
||||
],
|
||||
"features": "creation",
|
||||
"contains": [
|
||||
{
|
||||
"has": "telemetry"
|
||||
}
|
||||
],
|
||||
"model": {
|
||||
"composition": []
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"name": "Layout Grid",
|
||||
"control": "composite",
|
||||
"items": [
|
||||
{
|
||||
"name": "Horizontal grid (px)",
|
||||
"control": "textfield",
|
||||
"cssClass": "l-input-sm l-numeric"
|
||||
},
|
||||
{
|
||||
"name": "Vertical grid (px)",
|
||||
"control": "textfield",
|
||||
"cssClass": "l-input-sm l-numeric"
|
||||
}
|
||||
],
|
||||
"pattern": "^(\\d*[1-9]\\d*)?$",
|
||||
"property": "layoutGrid",
|
||||
"conversion": "number[]"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
ng-controller="FixedController as controller">
|
||||
|
||||
<!-- Background grid -->
|
||||
<div class="l-grid-holder" ng-click="controller.clearSelection()">
|
||||
<div class="l-grid-holder" ng-click="controller.bypassSelection($event)">
|
||||
<div class="l-grid l-grid-x"
|
||||
ng-if="!controller.getGridSize()[0] < 3"
|
||||
ng-style="{ 'background-size': controller.getGridSize() [0] + 'px 100%' }"></div>
|
||||
@ -35,33 +35,28 @@
|
||||
<!-- Fixed position elements -->
|
||||
<div ng-repeat="element in controller.getElements()"
|
||||
class="l-fixed-position-item s-selectable s-moveable s-hover-border"
|
||||
ng-class="{
|
||||
's-not-selected': controller.selected() && !controller.selected(element),
|
||||
's-selected': controller.selected(element)
|
||||
}"
|
||||
ng-style="element.style"
|
||||
ng-click="controller.select(element)">
|
||||
mct-selectable="controller.getContext(element)"
|
||||
mct-init-select="controller.shouldSelect(element)">
|
||||
<mct-include key="element.template"
|
||||
parameters="{ gridSize: controller.getGridSize() }"
|
||||
ng-model="element">
|
||||
</mct-include>
|
||||
</mct-include>
|
||||
</div>
|
||||
|
||||
<!-- Selection highlight, handles -->
|
||||
<span class="s-selected s-moveable" ng-if="controller.selected()">
|
||||
<span class="s-selected s-moveable" ng-if="controller.isElementSelected()">
|
||||
<div class="l-fixed-position-item t-edit-handle-holder"
|
||||
mct-drag-down="controller.moveHandle().startDrag(controller.selected())"
|
||||
mct-drag-down="controller.moveHandle().startDrag()"
|
||||
mct-drag="controller.moveHandle().continueDrag(delta)"
|
||||
mct-drag-up="controller.moveHandle().endDrag()"
|
||||
ng-style="controller.selected().style">
|
||||
mct-drag-up="controller.endDrag()"
|
||||
ng-style="controller.getSelectedElementStyle()">
|
||||
</div>
|
||||
<div ng-repeat="handle in controller.handles()"
|
||||
class="l-fixed-position-item-handle edit-corner"
|
||||
ng-style="handle.style()"
|
||||
mct-drag-down="handle.startDrag()"
|
||||
mct-drag="handle.continueDrag(delta)"
|
||||
mct-drag-up="handle.endDrag()">
|
||||
mct-drag-up="controller.endDrag(handle)">
|
||||
</div>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
@ -19,7 +19,7 @@
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div class="frame frame-template t-frame-inner abs t-object-type-{{ representation.selected.key }}">
|
||||
<div class="frame frame-template t-frame-inner abs t-object-type-{{ domainObject.getModel().type }}">
|
||||
<div class="abs object-browse-bar l-flex-row">
|
||||
<div class="left flex-elem l-flex-row grows">
|
||||
<mct-representation
|
||||
|
@ -22,10 +22,12 @@
|
||||
|
||||
<div class="abs l-layout"
|
||||
ng-controller="LayoutController as controller"
|
||||
ng-click="controller.clearSelection()">
|
||||
ng-click="controller.bypassSelection($event)">
|
||||
|
||||
<!-- Background grid -->
|
||||
<div class="l-grid-holder" ng-click="controller.clearSelection()">
|
||||
<div class="l-grid-holder"
|
||||
ng-show="!controller.drilledIn"
|
||||
ng-click="controller.bypassSelection($event)">
|
||||
<div class="l-grid l-grid-x"
|
||||
ng-if="!controller.getGridSize()[0] < 3"
|
||||
ng-style="{ 'background-size': controller.getGridSize() [0] + 'px 100%' }"></div>
|
||||
@ -34,10 +36,13 @@
|
||||
ng-style="{ 'background-size': '100% ' + controller.getGridSize() [1] + 'px' }"></div>
|
||||
</div>
|
||||
|
||||
<div class='abs frame t-frame-outer child-frame panel s-selectable s-moveable s-hover-border'
|
||||
ng-class="{ 'no-frame': !controller.hasFrame(childObject), 's-selected':controller.selected(childObject) }"
|
||||
<div class="abs frame t-frame-outer child-frame panel s-selectable s-moveable s-hover-border t-object-type-{{ childObject.getModel().type }}"
|
||||
data-layout-id="{{childObject.getId() + '-' + $id}}"
|
||||
ng-class="{ 'no-frame': !controller.hasFrame(childObject), 's-drilled-in': controller.isDrilledIn(childObject) }"
|
||||
ng-repeat="childObject in composition"
|
||||
ng-click="controller.select($event, childObject.getId())"
|
||||
ng-init="controller.selectIfNew(childObject.getId() + '-' + $id, childObject)"
|
||||
mct-selectable="controller.getContext(childObject, true)"
|
||||
ng-dblclick="controller.drill($event, childObject)"
|
||||
ng-style="controller.getFrameStyle(childObject.getId())">
|
||||
|
||||
<mct-representation key="'frame'"
|
||||
@ -45,7 +50,7 @@
|
||||
mct-object="childObject">
|
||||
</mct-representation>
|
||||
<!-- Drag handles -->
|
||||
<span class="abs t-edit-handle-holder s-hover-border" ng-if="controller.selected(childObject)">
|
||||
<span class="abs t-edit-handle-holder" ng-if="controller.selected(childObject) && !controller.isDrilledIn(childObject)">
|
||||
<span class="edit-handle edit-move"
|
||||
mct-drag-down="controller.startDrag(childObject.getId(), [1,1], [0,0])"
|
||||
mct-drag="controller.continueDrag(delta)"
|
||||
@ -73,7 +78,6 @@
|
||||
mct-drag-up="controller.endDrag()">
|
||||
</span>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -47,7 +47,7 @@ define(
|
||||
* @constructor
|
||||
* @param {Scope} $scope the controller's Angular scope
|
||||
*/
|
||||
function FixedController($scope, $q, dialogService, openmct) {
|
||||
function FixedController($scope, $q, dialogService, openmct, $element) {
|
||||
this.names = {}; // Cache names by ID
|
||||
this.values = {}; // Cache values by ID
|
||||
this.elementProxiesById = {};
|
||||
@ -55,9 +55,11 @@ define(
|
||||
this.telemetryObjects = [];
|
||||
this.subscriptions = [];
|
||||
this.openmct = openmct;
|
||||
this.$element = $element;
|
||||
this.$scope = $scope;
|
||||
|
||||
this.gridSize = $scope.domainObject && $scope.domainObject.getModel().layoutGrid;
|
||||
this.fixedViewSelectable = false;
|
||||
|
||||
var self = this;
|
||||
[
|
||||
@ -87,9 +89,8 @@ define(
|
||||
|
||||
// Update the style for a selected element
|
||||
function updateSelectionStyle() {
|
||||
var element = self.selection && self.selection.get();
|
||||
if (element) {
|
||||
element.style = convertPosition(element);
|
||||
if (self.selectedElementProxy) {
|
||||
self.selectedElementProxy.style = convertPosition(self.selectedElementProxy);
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,25 +137,19 @@ define(
|
||||
|
||||
// Decorate elements in the current configuration
|
||||
function refreshElements() {
|
||||
// Cache selection; we are instantiating new proxies
|
||||
// so we may want to restore this.
|
||||
var selected = self.selection && self.selection.get(),
|
||||
elements = (($scope.configuration || {}).elements || []),
|
||||
index = -1; // Start with a 'not-found' value
|
||||
|
||||
// Find the selection in the new array
|
||||
if (selected !== undefined) {
|
||||
index = elements.indexOf(selected.element);
|
||||
}
|
||||
var elements = (($scope.configuration || {}).elements || []);
|
||||
|
||||
// Create the new proxies...
|
||||
self.elementProxies = elements.map(makeProxyElement);
|
||||
|
||||
// Clear old selection, and restore if appropriate
|
||||
if (self.selection) {
|
||||
self.selection.deselect();
|
||||
if (index > -1) {
|
||||
self.select(self.elementProxies[index]);
|
||||
// If selection is not in array, select parent.
|
||||
// Otherwise, set the element to select after refresh.
|
||||
if (self.selectedElementProxy) {
|
||||
var index = elements.indexOf(self.selectedElementProxy.element);
|
||||
if (index === -1) {
|
||||
self.$element[0].click();
|
||||
} else if (!self.elementToSelectAfterRefresh) {
|
||||
self.elementToSelectAfterRefresh = self.elementProxies[index].element;
|
||||
}
|
||||
}
|
||||
|
||||
@ -224,12 +219,12 @@ define(
|
||||
$scope.configuration.elements || [];
|
||||
// Store the position of this element.
|
||||
$scope.configuration.elements.push(element);
|
||||
|
||||
self.elementToSelectAfterRefresh = element;
|
||||
|
||||
// Refresh displayed elements
|
||||
refreshElements();
|
||||
// Select the newly-added element
|
||||
self.select(
|
||||
self.elementProxies[self.elementProxies.length - 1]
|
||||
);
|
||||
|
||||
// Mark change as persistable
|
||||
if ($scope.commit) {
|
||||
$scope.commit("Dropped an element.");
|
||||
@ -263,21 +258,36 @@ define(
|
||||
self.getTelemetry($scope.domainObject);
|
||||
}
|
||||
|
||||
// Sets the selectable object in response to the selection change event.
|
||||
function setSelection(selectable) {
|
||||
var selection = selectable[0];
|
||||
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.context.elementProxy) {
|
||||
self.selectedElementProxy = selection.context.elementProxy;
|
||||
self.mvHandle = self.generateDragHandle(self.selectedElementProxy);
|
||||
self.resizeHandles = self.generateDragHandles(self.selectedElementProxy);
|
||||
} else {
|
||||
// Make fixed view selectable if it's not already.
|
||||
if (!self.fixedViewSelectable && selectable.length === 1) {
|
||||
self.fixedViewSelectable = true;
|
||||
selection.context.viewProxy = new FixedProxy(addElement, $q, dialogService);
|
||||
self.openmct.selection.select(selection);
|
||||
}
|
||||
|
||||
self.resizeHandles = [];
|
||||
self.mvHandle = undefined;
|
||||
self.selectedElementProxy = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this.elementProxies = [];
|
||||
this.generateDragHandle = generateDragHandle;
|
||||
this.generateDragHandles = generateDragHandles;
|
||||
|
||||
// Track current selection state
|
||||
$scope.$watch("selection", function (selection) {
|
||||
this.selection = selection;
|
||||
|
||||
// Expose the view's selection proxy
|
||||
if (this.selection) {
|
||||
this.selection.proxy(
|
||||
new FixedProxy(addElement, $q, dialogService)
|
||||
);
|
||||
}
|
||||
}.bind(this));
|
||||
this.updateSelectionStyle = updateSelectionStyle;
|
||||
|
||||
// Detect changes to grid size
|
||||
$scope.$watch("model.layoutGrid", updateElementPositions);
|
||||
@ -298,10 +308,13 @@ define(
|
||||
$scope.$on("$destroy", function () {
|
||||
self.unsubscribe();
|
||||
self.openmct.time.off("bounds", updateDisplayBounds);
|
||||
self.openmct.selection.off("change", setSelection);
|
||||
});
|
||||
|
||||
// Respond to external bounds changes
|
||||
this.openmct.time.on("bounds", updateDisplayBounds);
|
||||
this.openmct.selection.on('change', setSelection);
|
||||
this.$element.on('click', this.bypassSelection.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -492,38 +505,56 @@ define(
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the element is currently selected, or (if no
|
||||
* argument is supplied) get the currently selected element.
|
||||
* @returns {boolean} true if selected
|
||||
* Checks if the element should be selected or not.
|
||||
*
|
||||
* @param elementProxy the element to check
|
||||
* @returns {boolean} true if the element should be selected.
|
||||
*/
|
||||
FixedController.prototype.selected = function (element) {
|
||||
var selection = this.selection;
|
||||
return selection && ((arguments.length > 0) ?
|
||||
selection.selected(element) : selection.get());
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the active user selection in this view.
|
||||
* @param element the element to select
|
||||
*/
|
||||
FixedController.prototype.select = function select(element) {
|
||||
if (this.selection) {
|
||||
// Update selection...
|
||||
this.selection.select(element);
|
||||
// ...as well as move, resize handles
|
||||
this.mvHandle = this.generateDragHandle(element);
|
||||
this.resizeHandles = this.generateDragHandles(element);
|
||||
FixedController.prototype.shouldSelect = function (elementProxy) {
|
||||
if (elementProxy.element === this.elementToSelectAfterRefresh) {
|
||||
delete this.elementToSelectAfterRefresh;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the current user selection.
|
||||
* Checks if an element is currently selected.
|
||||
*
|
||||
* @returns {boolean} true if an element is selected.
|
||||
*/
|
||||
FixedController.prototype.clearSelection = function () {
|
||||
if (this.selection) {
|
||||
this.selection.deselect();
|
||||
this.resizeHandles = [];
|
||||
this.mvHandle = undefined;
|
||||
FixedController.prototype.isElementSelected = function () {
|
||||
return (this.selectedElementProxy) ? true : false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the style for the selected element.
|
||||
*
|
||||
* @returns {string} element style
|
||||
*/
|
||||
FixedController.prototype.getSelectedElementStyle = function () {
|
||||
return (this.selectedElementProxy) ? this.selectedElementProxy.style : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the selected element.
|
||||
*
|
||||
* @returns the selected element
|
||||
*/
|
||||
FixedController.prototype.getSelectedElement = function () {
|
||||
return this.selectedElementProxy;
|
||||
};
|
||||
|
||||
/**
|
||||
* Prevents the event from bubbling up if drag is in progress.
|
||||
*/
|
||||
FixedController.prototype.bypassSelection = function ($event) {
|
||||
if (this.dragInProgress) {
|
||||
if ($event) {
|
||||
$event.stopPropagation();
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@ -544,6 +575,38 @@ define(
|
||||
return this.mvHandle;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the selection context.
|
||||
*
|
||||
* @param elementProxy the element proxy
|
||||
* @returns {object} the context object which includes elementProxy and toolbar
|
||||
*/
|
||||
FixedController.prototype.getContext = function (elementProxy) {
|
||||
return {
|
||||
elementProxy: elementProxy,
|
||||
toolbar: elementProxy
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* End drag.
|
||||
*
|
||||
* @param handle the resize handle
|
||||
*/
|
||||
FixedController.prototype.endDrag = function (handle) {
|
||||
this.dragInProgress = true;
|
||||
|
||||
setTimeout(function () {
|
||||
this.dragInProgress = false;
|
||||
}.bind(this), 0);
|
||||
|
||||
if (handle) {
|
||||
handle.endDrag();
|
||||
} else {
|
||||
this.moveHandle().endDrag();
|
||||
}
|
||||
};
|
||||
|
||||
return FixedController;
|
||||
}
|
||||
);
|
||||
|
@ -65,7 +65,7 @@ define(
|
||||
* Start a drag gesture. This should be called when a drag
|
||||
* begins to track initial state.
|
||||
*/
|
||||
FixedDragHandle.prototype.startDrag = function startDrag() {
|
||||
FixedDragHandle.prototype.startDrag = function () {
|
||||
// Cache initial x/y positions
|
||||
this.dragging = {
|
||||
x: this.elementHandle.x(),
|
||||
|
@ -27,9 +27,11 @@
|
||||
*/
|
||||
define(
|
||||
[
|
||||
'zepto',
|
||||
'./LayoutDrag'
|
||||
],
|
||||
function (
|
||||
$,
|
||||
LayoutDrag
|
||||
) {
|
||||
|
||||
@ -50,10 +52,12 @@ define(
|
||||
* @constructor
|
||||
* @param {Scope} $scope the controller's Angular scope
|
||||
*/
|
||||
function LayoutController($scope) {
|
||||
function LayoutController($scope, $element, openmct) {
|
||||
var self = this,
|
||||
callbackCount = 0;
|
||||
|
||||
this.$element = $element;
|
||||
|
||||
// Update grid size when it changed
|
||||
function updateGridSize(layoutGrid) {
|
||||
var oldSize = self.gridSize;
|
||||
@ -123,12 +127,11 @@ define(
|
||||
self.layoutPanels(ids);
|
||||
self.setFrames(ids);
|
||||
|
||||
// If there is a newly-dropped object, select it.
|
||||
if (self.droppedIdToSelectAfterRefresh) {
|
||||
self.select(null, self.droppedIdToSelectAfterRefresh);
|
||||
delete self.droppedIdToSelectAfterRefresh;
|
||||
} else if (composition.indexOf(self.selectedId) === -1) {
|
||||
self.clearSelection();
|
||||
if (self.selectedId &&
|
||||
self.selectedId !== $scope.domainObject.getId() &&
|
||||
composition.indexOf(self.selectedId) === -1) {
|
||||
// Click triggers selection of layout parent.
|
||||
self.$element[0].click();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -160,22 +163,39 @@ define(
|
||||
}
|
||||
};
|
||||
|
||||
// Sets the selectable object in response to the selection change event.
|
||||
function setSelection(selectable) {
|
||||
var selection = selectable[0];
|
||||
|
||||
if (!selection) {
|
||||
delete self.selectedId;
|
||||
return;
|
||||
}
|
||||
|
||||
self.selectedId = selection.context.oldItem.getId();
|
||||
self.drilledIn = undefined;
|
||||
self.selectable = selectable;
|
||||
}
|
||||
|
||||
this.positions = {};
|
||||
this.rawPositions = {};
|
||||
this.gridSize = DEFAULT_GRID_SIZE;
|
||||
this.$scope = $scope;
|
||||
this.drilledIn = undefined;
|
||||
this.openmct = openmct;
|
||||
|
||||
// Watch for changes to the grid size in the model
|
||||
$scope.$watch("model.layoutGrid", updateGridSize);
|
||||
|
||||
$scope.$watch("selection", function (selection) {
|
||||
this.selection = selection;
|
||||
}.bind(this));
|
||||
|
||||
// Update composed objects on screen, and position panes
|
||||
$scope.$watchCollection("model.composition", refreshComposition);
|
||||
|
||||
// Position panes where they are dropped
|
||||
openmct.selection.on('change', setSelection);
|
||||
|
||||
$scope.$on("$destroy", function () {
|
||||
openmct.selection.off("change", setSelection);
|
||||
});
|
||||
|
||||
$scope.$on("mctDrop", handleDrop);
|
||||
}
|
||||
|
||||
@ -357,37 +377,14 @@ define(
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the object is currently selected.
|
||||
* Checks if the object is currently selected.
|
||||
*
|
||||
* @param {string} obj the object to check for selection
|
||||
* @returns {boolean} true if selected, otherwise false
|
||||
*/
|
||||
LayoutController.prototype.selected = function (obj) {
|
||||
return !!this.selectedId && this.selectedId === obj.getId();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the active user selection in this view.
|
||||
*
|
||||
* @param event the mouse event
|
||||
* @param {string} id the object id
|
||||
*/
|
||||
LayoutController.prototype.select = function (event, id) {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
if (this.selection) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedId = id;
|
||||
|
||||
var selectedObj = {};
|
||||
selectedObj[this.frames[id] ? 'hideFrame' : 'showFrame'] = this.toggleFrame.bind(this, id);
|
||||
|
||||
if (this.selection) {
|
||||
this.selection.select(selectedObj);
|
||||
}
|
||||
var sobj = this.openmct.selection.get()[0];
|
||||
return (sobj && sobj.context.oldItem.getId() === obj.getId()) ? true : false;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -396,7 +393,7 @@ define(
|
||||
* @param {string} id the object id
|
||||
* @private
|
||||
*/
|
||||
LayoutController.prototype.toggleFrame = function (id) {
|
||||
LayoutController.prototype.toggleFrame = function (id, domainObject) {
|
||||
var configuration = this.$scope.configuration;
|
||||
|
||||
if (!configuration.panels[id]) {
|
||||
@ -404,21 +401,75 @@ define(
|
||||
}
|
||||
|
||||
this.frames[id] = configuration.panels[id].hasFrame = !this.frames[id];
|
||||
this.select(undefined, id); // reselect so toolbar updates
|
||||
|
||||
var selection = this.openmct.selection.get();
|
||||
selection[0].context.toolbar = this.getToolbar(id, domainObject);
|
||||
this.openmct.selection.select(selection); // reselect so toolbar updates
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the current user selection.
|
||||
* Gets the toolbar object for the given domain object.
|
||||
*
|
||||
* @param id the domain object id
|
||||
* @param domainObject the domain object
|
||||
* @returns {object}
|
||||
* @private
|
||||
*/
|
||||
LayoutController.prototype.clearSelection = function () {
|
||||
LayoutController.prototype.getToolbar = function (id, domainObject) {
|
||||
var toolbarObj = {};
|
||||
toolbarObj[this.frames[id] ? 'hideFrame' : 'showFrame'] = this.toggleFrame.bind(this, id, domainObject);
|
||||
return toolbarObj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Bypasses selection if drag is in progress.
|
||||
*
|
||||
* @param event the angular event object
|
||||
*/
|
||||
LayoutController.prototype.bypassSelection = function (event) {
|
||||
if (this.dragInProgress) {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the domain object is drilled in.
|
||||
*
|
||||
* @param domainObject the domain object
|
||||
* @return true if the object is drilled in, false otherwise
|
||||
*/
|
||||
LayoutController.prototype.isDrilledIn = function (domainObject) {
|
||||
return this.drilledIn === domainObject.getId();
|
||||
};
|
||||
|
||||
/**
|
||||
* Puts the given object in the drilled-in mode.
|
||||
*
|
||||
* @param event the angular event object
|
||||
* @param domainObject the domain object
|
||||
*/
|
||||
LayoutController.prototype.drill = function (event, domainObject) {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (!domainObject.getCapability('editor').inEditContext()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selection) {
|
||||
this.selection.deselect();
|
||||
delete this.selectedId;
|
||||
if (!domainObject.hasCapability('composition')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable since fixed position doesn't use the selection API yet
|
||||
if (domainObject.getModel().type === 'telemetry.fixed') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.drilledIn = domainObject.getId();
|
||||
};
|
||||
|
||||
/**
|
||||
@ -440,6 +491,36 @@ define(
|
||||
return this.gridSize;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the selection context.
|
||||
*
|
||||
* @param domainObject the domain object
|
||||
* @returns {object} the context object which includes
|
||||
* item, oldItem and toolbar
|
||||
*/
|
||||
LayoutController.prototype.getContext = function (domainObject, toolbar) {
|
||||
return {
|
||||
item: domainObject.useCapability('adapter'),
|
||||
oldItem: domainObject,
|
||||
toolbar: toolbar ? this.getToolbar(domainObject.getId(), domainObject) : undefined
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Selects a newly-dropped object.
|
||||
*
|
||||
* @param classSelector the css class selector
|
||||
* @param domainObject the domain object
|
||||
*/
|
||||
LayoutController.prototype.selectIfNew = function (selector, domainObject) {
|
||||
if (domainObject.getId() === this.droppedIdToSelectAfterRefresh) {
|
||||
setTimeout(function () {
|
||||
$('[data-layout-id="' + selector + '"]')[0].click();
|
||||
delete this.droppedIdToSelectAfterRefresh;
|
||||
}.bind(this), 0);
|
||||
}
|
||||
};
|
||||
|
||||
return LayoutController;
|
||||
}
|
||||
);
|
||||
|
@ -55,8 +55,8 @@ define(
|
||||
* @param element the fixed position element, as stored in its
|
||||
* configuration
|
||||
* @param index the element's index within its array
|
||||
* @param {number[]} gridSize the current layout grid size in [x,y] from
|
||||
* @param {Array} elements the full array of elements
|
||||
* @param {number[]} gridSize the current layout grid size in [x,y] from
|
||||
*/
|
||||
function ElementProxy(element, index, elements, gridSize) {
|
||||
/**
|
||||
|
@ -21,8 +21,14 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../src/FixedController"],
|
||||
function (FixedController) {
|
||||
[
|
||||
"../src/FixedController",
|
||||
"zepto"
|
||||
],
|
||||
function (
|
||||
FixedController,
|
||||
$
|
||||
) {
|
||||
|
||||
describe("The Fixed Position controller", function () {
|
||||
var mockScope,
|
||||
@ -46,6 +52,9 @@ define(
|
||||
mockMetadata,
|
||||
mockTimeSystem,
|
||||
mockLimitEvaluator,
|
||||
mockSelection,
|
||||
$element = [],
|
||||
selectable = [],
|
||||
controller;
|
||||
|
||||
// Utility function; find a watch for a given expression
|
||||
@ -180,17 +189,30 @@ define(
|
||||
|
||||
mockScope.model = testModel;
|
||||
mockScope.configuration = testConfiguration;
|
||||
mockScope.selection = jasmine.createSpyObj(
|
||||
'selection',
|
||||
['select', 'get', 'selected', 'deselect', 'proxy']
|
||||
);
|
||||
|
||||
selectable[0] = {
|
||||
context: {
|
||||
oldItem: mockDomainObject
|
||||
}
|
||||
};
|
||||
mockSelection = jasmine.createSpyObj("selection", [
|
||||
'select',
|
||||
'on',
|
||||
'off',
|
||||
'get'
|
||||
]);
|
||||
mockSelection.get.andCallThrough();
|
||||
|
||||
mockOpenMCT = {
|
||||
time: mockConductor,
|
||||
telemetry: mockTelemetryAPI,
|
||||
composition: mockCompositionAPI
|
||||
composition: mockCompositionAPI,
|
||||
selection: mockSelection
|
||||
};
|
||||
|
||||
$element = $('<div></div>');
|
||||
spyOn($element[0], 'click');
|
||||
|
||||
mockMetadata = jasmine.createSpyObj('mockMetadata', [
|
||||
'valuesForHints',
|
||||
'value',
|
||||
@ -226,11 +248,11 @@ define(
|
||||
mockScope,
|
||||
mockQ,
|
||||
mockDialogService,
|
||||
mockOpenMCT
|
||||
mockOpenMCT,
|
||||
$element
|
||||
);
|
||||
|
||||
findWatch("model.layoutGrid")(testModel.layoutGrid);
|
||||
findWatch("selection")(mockScope.selection);
|
||||
});
|
||||
|
||||
it("subscribes when a domain object is available", function () {
|
||||
@ -306,41 +328,41 @@ define(
|
||||
});
|
||||
|
||||
it("allows elements to be selected", function () {
|
||||
var elements;
|
||||
|
||||
testModel.modified = 1;
|
||||
findWatch("model.modified")(testModel.modified);
|
||||
|
||||
elements = controller.getElements();
|
||||
controller.select(elements[1]);
|
||||
expect(mockScope.selection.select)
|
||||
.toHaveBeenCalledWith(elements[1]);
|
||||
selectable[0].context.elementProxy = controller.getElements()[1];
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(controller.isElementSelected()).toBe(true);
|
||||
});
|
||||
|
||||
it("allows selection retrieval", function () {
|
||||
// selected with no arguments should give the current
|
||||
// selection
|
||||
var elements;
|
||||
|
||||
testModel.modified = 1;
|
||||
findWatch("model.modified")(testModel.modified);
|
||||
|
||||
elements = controller.getElements();
|
||||
controller.select(elements[1]);
|
||||
mockScope.selection.get.andReturn(elements[1]);
|
||||
expect(controller.selected()).toEqual(elements[1]);
|
||||
selectable[0].context.elementProxy = elements[1];
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(controller.getSelectedElement()).toEqual(elements[1]);
|
||||
});
|
||||
|
||||
it("allows selections to be cleared", function () {
|
||||
var elements;
|
||||
|
||||
it("selects the parent view when selected element is removed", function () {
|
||||
testModel.modified = 1;
|
||||
findWatch("model.modified")(testModel.modified);
|
||||
|
||||
elements = controller.getElements();
|
||||
controller.select(elements[1]);
|
||||
controller.clearSelection();
|
||||
expect(controller.selected(elements[1])).toBeFalsy();
|
||||
var elements = controller.getElements();
|
||||
selectable[0].context.elementProxy = elements[1];
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
elements[1].remove();
|
||||
testModel.modified = 2;
|
||||
findWatch("model.modified")(testModel.modified);
|
||||
|
||||
expect($element[0].click).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retains selections during refresh", function () {
|
||||
@ -352,23 +374,21 @@ define(
|
||||
findWatch("model.modified")(testModel.modified);
|
||||
|
||||
elements = controller.getElements();
|
||||
controller.select(elements[1]);
|
||||
selectable[0].context.elementProxy = elements[1];
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
// Verify precondition
|
||||
expect(mockScope.selection.select.calls.length).toEqual(1);
|
||||
|
||||
// Mimic selection behavior
|
||||
mockScope.selection.get.andReturn(elements[1]);
|
||||
expect(controller.getSelectedElement()).toEqual(elements[1]);
|
||||
|
||||
elements[2].remove();
|
||||
testModel.modified = 2;
|
||||
findWatch("model.modified")(testModel.modified);
|
||||
|
||||
elements = controller.getElements();
|
||||
|
||||
// Verify removal, as test assumes this
|
||||
expect(elements.length).toEqual(2);
|
||||
|
||||
expect(mockScope.selection.select.calls.length).toEqual(2);
|
||||
expect(controller.shouldSelect(elements[1])).toBe(true);
|
||||
});
|
||||
|
||||
it("Displays received values for telemetry elements", function () {
|
||||
@ -505,21 +525,25 @@ define(
|
||||
});
|
||||
|
||||
it("exposes a view-level selection proxy", function () {
|
||||
expect(mockScope.selection.proxy).toHaveBeenCalledWith(
|
||||
jasmine.any(Object)
|
||||
);
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
var selection = mockOpenMCT.selection.select.mostRecentCall.args[0];
|
||||
|
||||
expect(mockOpenMCT.selection.select).toHaveBeenCalled();
|
||||
expect(selection.context.viewProxy).toBeDefined();
|
||||
});
|
||||
|
||||
it("exposes drag handles", function () {
|
||||
var handles;
|
||||
|
||||
// Select something so that drag handles are expected
|
||||
testModel.modified = 1;
|
||||
findWatch("model.modified")(testModel.modified);
|
||||
controller.select(controller.getElements()[1]);
|
||||
|
||||
selectable[0].context.elementProxy = controller.getElements()[1];
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
// Should have a non-empty array of handles
|
||||
handles = controller.handles();
|
||||
|
||||
expect(handles).toEqual(jasmine.any(Array));
|
||||
expect(handles.length).not.toEqual(0);
|
||||
|
||||
@ -532,15 +556,14 @@ define(
|
||||
});
|
||||
|
||||
it("exposes a move handle", function () {
|
||||
var handle;
|
||||
|
||||
// Select something so that drag handles are expected
|
||||
testModel.modified = 1;
|
||||
findWatch("model.modified")(testModel.modified);
|
||||
controller.select(controller.getElements()[1]);
|
||||
|
||||
selectable[0].context.elementProxy = controller.getElements()[1];
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
// Should have a move handle
|
||||
handle = controller.moveHandle();
|
||||
var handle = controller.moveHandle();
|
||||
|
||||
// And it should have start/continue/end drag methods
|
||||
expect(handle.startDrag).toEqual(jasmine.any(Function));
|
||||
@ -551,26 +574,40 @@ define(
|
||||
it("updates selection style during drag", function () {
|
||||
var oldStyle;
|
||||
|
||||
// Select something so that drag handles are expected
|
||||
testModel.modified = 1;
|
||||
findWatch("model.modified")(testModel.modified);
|
||||
controller.select(controller.getElements()[1]);
|
||||
mockScope.selection.get.andReturn(controller.getElements()[1]);
|
||||
|
||||
selectable[0].context.elementProxy = controller.getElements()[1];
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
// Get style
|
||||
oldStyle = controller.selected().style;
|
||||
oldStyle = controller.getSelectedElementStyle();
|
||||
|
||||
// Start a drag gesture
|
||||
controller.moveHandle().startDrag();
|
||||
|
||||
// Haven't moved yet; style shouldn't have updated yet
|
||||
expect(controller.selected().style).toEqual(oldStyle);
|
||||
expect(controller.getSelectedElementStyle()).toEqual(oldStyle);
|
||||
|
||||
// Drag a little
|
||||
controller.moveHandle().continueDrag([1000, 100]);
|
||||
|
||||
// Style should have been updated
|
||||
expect(controller.selected().style).not.toEqual(oldStyle);
|
||||
expect(controller.getSelectedElementStyle()).not.toEqual(oldStyle);
|
||||
});
|
||||
|
||||
it("cleans up slection on scope destroy", function () {
|
||||
expect(mockScope.$on).toHaveBeenCalledWith(
|
||||
'$destroy',
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
mockScope.$on.mostRecentCall.args[1]();
|
||||
|
||||
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
|
||||
'change',
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
describe("on display bounds changes", function () {
|
||||
@ -702,6 +739,14 @@ define(
|
||||
expect(controller.getElements()[0].cssClass).toEqual("alarm-a");
|
||||
});
|
||||
});
|
||||
|
||||
it("listens for selection change events", function () {
|
||||
expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
|
||||
'change',
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -21,8 +21,14 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../src/LayoutController"],
|
||||
function (LayoutController) {
|
||||
[
|
||||
"../src/LayoutController",
|
||||
"zepto"
|
||||
],
|
||||
function (
|
||||
LayoutController,
|
||||
$
|
||||
) {
|
||||
|
||||
describe("The Layout controller", function () {
|
||||
var mockScope,
|
||||
@ -32,7 +38,12 @@ define(
|
||||
controller,
|
||||
mockCompositionCapability,
|
||||
mockComposition,
|
||||
mockCompositionObjects;
|
||||
mockCompositionObjects,
|
||||
mockOpenMCT,
|
||||
mockSelection,
|
||||
mockDomainObjectCapability,
|
||||
$element = [],
|
||||
selectable = [];
|
||||
|
||||
function mockPromise(value) {
|
||||
return {
|
||||
@ -58,21 +69,18 @@ define(
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
getCapability: function () {
|
||||
return mockDomainObjectCapability;
|
||||
},
|
||||
hasCapability: function (param) {
|
||||
if (param === 'composition') {
|
||||
return id !== 'b';
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Utility function to find a watch for a given expression
|
||||
function findWatch(expr) {
|
||||
var watch;
|
||||
mockScope.$watch.calls.forEach(function (call) {
|
||||
if (call.args[0] === expr) {
|
||||
watch = call.args[1];
|
||||
}
|
||||
});
|
||||
return watch;
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
@ -88,7 +96,6 @@ define(
|
||||
mockComposition = ["a", "b", "c"];
|
||||
mockCompositionObjects = mockComposition.map(mockDomainObject);
|
||||
|
||||
|
||||
testConfiguration = {
|
||||
panels: {
|
||||
a: {
|
||||
@ -97,27 +104,70 @@ define(
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mockDomainObjectCapability = jasmine.createSpyObj('capability',
|
||||
['inEditContext']
|
||||
);
|
||||
mockCompositionCapability = mockPromise(mockCompositionObjects);
|
||||
|
||||
mockScope.domainObject = mockDomainObject("mockDomainObject");
|
||||
mockScope.model = testModel;
|
||||
mockScope.configuration = testConfiguration;
|
||||
mockScope.selection = jasmine.createSpyObj(
|
||||
'selection',
|
||||
['select', 'get', 'selected', 'deselect']
|
||||
);
|
||||
|
||||
selectable[0] = {
|
||||
context: {
|
||||
oldItem: mockScope.domainObject
|
||||
}
|
||||
};
|
||||
|
||||
mockSelection = jasmine.createSpyObj("selection", [
|
||||
'select',
|
||||
'on',
|
||||
'off',
|
||||
'get'
|
||||
]);
|
||||
mockSelection.get.andReturn(selectable);
|
||||
mockOpenMCT = {
|
||||
selection: mockSelection
|
||||
};
|
||||
|
||||
$element = $('<div></div>');
|
||||
$(document).find('body').append($element);
|
||||
spyOn($element[0], 'click');
|
||||
|
||||
spyOn(mockScope.domainObject, "useCapability").andCallThrough();
|
||||
|
||||
controller = new LayoutController(mockScope);
|
||||
controller = new LayoutController(mockScope, $element, mockOpenMCT);
|
||||
spyOn(controller, "layoutPanels").andCallThrough();
|
||||
|
||||
findWatch("selection")(mockScope.selection);
|
||||
|
||||
jasmine.Clock.useMock();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
$element.remove();
|
||||
});
|
||||
|
||||
|
||||
it("listens for selection change events", function () {
|
||||
expect(mockOpenMCT.selection.on).toHaveBeenCalledWith(
|
||||
'change',
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("cleans up on scope destroy", function () {
|
||||
expect(mockScope.$on).toHaveBeenCalledWith(
|
||||
'$destroy',
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
mockScope.$on.calls[0].args[1]();
|
||||
|
||||
expect(mockOpenMCT.selection.off).toHaveBeenCalledWith(
|
||||
'change',
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
// Model changes will indicate that panel positions
|
||||
// may have changed, for instance.
|
||||
it("watches for changes to composition", function () {
|
||||
@ -320,67 +370,35 @@ define(
|
||||
.not.toEqual(oldStyle);
|
||||
});
|
||||
|
||||
it("allows panels to be selected", function () {
|
||||
it("allows objects to be selected", function () {
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
var childObj = mockCompositionObjects[0];
|
||||
selectable[0].context.oldItem = childObj;
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
expect(controller.selected(childObj)).toBe(true);
|
||||
});
|
||||
|
||||
it("prevents event bubbling while drag is in progress", function () {
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
var childObj = mockCompositionObjects[0];
|
||||
|
||||
controller.select(mockEvent, childObj.getId());
|
||||
// Do a drag
|
||||
controller.startDrag(childObj.getId(), [1, 1], [0, 0]);
|
||||
controller.continueDrag([100, 100]);
|
||||
controller.endDrag();
|
||||
|
||||
// Because mouse position could cause the parent object to be selected, this should be ignored.
|
||||
controller.bypassSelection(mockEvent);
|
||||
|
||||
expect(mockEvent.stopPropagation).toHaveBeenCalled();
|
||||
|
||||
expect(controller.selected(childObj)).toBe(true);
|
||||
});
|
||||
|
||||
it("allows selection to be cleared", function () {
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
var childObj = mockCompositionObjects[0];
|
||||
|
||||
controller.select(null, childObj.getId());
|
||||
controller.clearSelection();
|
||||
|
||||
expect(controller.selected(childObj)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("prevents clearing selection while drag is in progress", function () {
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
var childObj = mockCompositionObjects[0];
|
||||
var id = childObj.getId();
|
||||
|
||||
controller.select(mockEvent, id);
|
||||
|
||||
// Do a drag
|
||||
controller.startDrag(id, [1, 1], [0, 0]);
|
||||
controller.continueDrag([100, 100]);
|
||||
controller.endDrag();
|
||||
|
||||
// Because mouse position could cause clearSelection to be called, this should be ignored.
|
||||
controller.clearSelection();
|
||||
|
||||
expect(controller.selected(childObj)).toBe(true);
|
||||
|
||||
// Shoud be able to clear the selection after dragging is done.
|
||||
// Shoud be able to select another object when dragging is done.
|
||||
jasmine.Clock.tick(0);
|
||||
controller.clearSelection();
|
||||
mockEvent.stopPropagation.reset();
|
||||
controller.bypassSelection(mockEvent);
|
||||
|
||||
expect(controller.selected(childObj)).toBe(false);
|
||||
});
|
||||
|
||||
it("clears selection after moving/resizing", function () {
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
var childObj = mockCompositionObjects[0];
|
||||
var id = childObj.getId();
|
||||
|
||||
controller.select(mockEvent, id);
|
||||
|
||||
// Do a drag
|
||||
controller.startDrag(id, [1, 1], [0, 0]);
|
||||
controller.continueDrag([100, 100]);
|
||||
controller.endDrag();
|
||||
|
||||
jasmine.Clock.tick(0);
|
||||
controller.clearSelection();
|
||||
|
||||
expect(controller.selected(childObj)).toBe(false);
|
||||
expect(mockEvent.stopPropagation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows frames by default", function () {
|
||||
@ -398,43 +416,74 @@ define(
|
||||
it("hides frame when selected object has frame ", function () {
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
var childObj = mockCompositionObjects[0];
|
||||
controller.select(mockEvent, childObj.getId());
|
||||
|
||||
expect(mockScope.selection.select).toHaveBeenCalled();
|
||||
|
||||
var selectedObj = mockScope.selection.select.mostRecentCall.args[0];
|
||||
selectable[0].context.oldItem = childObj;
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
var toolbarObj = controller.getToolbar(childObj.getId(), childObj);
|
||||
|
||||
expect(controller.hasFrame(childObj)).toBe(true);
|
||||
expect(selectedObj.hideFrame).toBeDefined();
|
||||
expect(selectedObj.hideFrame).toEqual(jasmine.any(Function));
|
||||
expect(toolbarObj.hideFrame).toBeDefined();
|
||||
expect(toolbarObj.hideFrame).toEqual(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("shows frame when selected object has no frame", function () {
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
|
||||
var childObj = mockCompositionObjects[1];
|
||||
controller.select(mockEvent, childObj.getId());
|
||||
|
||||
expect(mockScope.selection.select).toHaveBeenCalled();
|
||||
|
||||
var selectedObj = mockScope.selection.select.mostRecentCall.args[0];
|
||||
selectable[0].context.oldItem = childObj;
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
var toolbarObj = controller.getToolbar(childObj.getId(), childObj);
|
||||
|
||||
expect(controller.hasFrame(childObj)).toBe(false);
|
||||
expect(selectedObj.showFrame).toBeDefined();
|
||||
expect(selectedObj.showFrame).toEqual(jasmine.any(Function));
|
||||
expect(toolbarObj.showFrame).toBeDefined();
|
||||
expect(toolbarObj.showFrame).toEqual(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it("deselects the object that is no longer in the composition", function () {
|
||||
it("selects the parent object when selected object is removed", function () {
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
var childObj = mockCompositionObjects[0];
|
||||
controller.select(mockEvent, childObj.getId());
|
||||
selectable[0].context.oldItem = childObj;
|
||||
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
|
||||
|
||||
var composition = ["b", "c"];
|
||||
mockScope.$watchCollection.mostRecentCall.args[1](composition);
|
||||
|
||||
expect(controller.selected(childObj)).toBe(false);
|
||||
expect($element[0].click).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows objects to be drilled-in only when editing", function () {
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
var childObj = mockCompositionObjects[0];
|
||||
childObj.getCapability().inEditContext.andReturn(false);
|
||||
controller.drill(mockEvent, childObj);
|
||||
|
||||
expect(controller.isDrilledIn(childObj)).toBe(false);
|
||||
});
|
||||
|
||||
it("allows objects to be drilled-in only if it has sub objects", function () {
|
||||
mockScope.$watchCollection.mostRecentCall.args[1]();
|
||||
var childObj = mockCompositionObjects[1];
|
||||
childObj.getCapability().inEditContext.andReturn(true);
|
||||
controller.drill(mockEvent, childObj);
|
||||
|
||||
expect(controller.isDrilledIn(childObj)).toBe(false);
|
||||
});
|
||||
|
||||
it("selects a newly-dropped object", function () {
|
||||
mockScope.$on.mostRecentCall.args[1](
|
||||
mockEvent,
|
||||
'd',
|
||||
{ x: 300, y: 100 }
|
||||
);
|
||||
|
||||
var childObj = mockDomainObject("d");
|
||||
var testElement = $("<div data-layout-id='some-id'></div>");
|
||||
$element.append(testElement);
|
||||
spyOn(testElement[0], 'click');
|
||||
|
||||
controller.selectIfNew('some-id', childObj);
|
||||
jasmine.Clock.tick(0);
|
||||
|
||||
expect(testElement[0].click).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -1,37 +0,0 @@
|
||||
# Plot README
|
||||
|
||||
## Chart
|
||||
|
||||
The `mct-chart` directive is used to support drawing of simple charts. It is
|
||||
present to support the Plot view, and its functionality is limited to the
|
||||
functionality that is relevant for that view.
|
||||
|
||||
This directive is used at the element level and takes one attribute, `draw`
|
||||
which is an Angular expression which will should evaluate to a drawing object.
|
||||
This drawing object should contain the following properties:
|
||||
|
||||
* `dimensions`: The size, in logical coordinates, of the chart area. A
|
||||
two-element array or numbers.
|
||||
* `origin`: The position, in logical coordinates, of the lower-left corner of
|
||||
the chart area. A two-element array or numbers.
|
||||
* `lines`: An array of lines (e.g. as a plot line) to draw, where each line is
|
||||
expressed as an object containing:
|
||||
* `buffer`: A Float32Array containing points in the line, in logical
|
||||
coordinates, in sequential x,y pairs.
|
||||
* `color`: The color of the line, as a four-element RGBA array, where
|
||||
each element is a number in the range of 0.0-1.0.
|
||||
* `points`: The number of points in the line.
|
||||
* `boxes`: An array of rectangles to draw in the chart area. Each is an object
|
||||
containing:
|
||||
* `start`: The first corner of the rectangle, as a two-element array of
|
||||
numbers, in logical coordinates.
|
||||
* `end`: The opposite corner of the rectangle, as a two-element array of
|
||||
numbers, in logical coordinates. color : The color of the line, as a
|
||||
four-element RGBA array, where each element is a number in the range of
|
||||
0.0-1.0.
|
||||
|
||||
While `mct-chart` is intended to support plots specifically, it does perform
|
||||
some useful management of canvas objects (e.g. choosing between WebGL and Canvas
|
||||
2D APIs for drawing based on browser support) so its usage is recommended when
|
||||
its supported drawing primitives are sufficient for other charting tasks.
|
||||
|
@ -1,157 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2017, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
"./src/MCTChart",
|
||||
"./src/PlotController",
|
||||
"./src/policies/PlotViewPolicy",
|
||||
"./src/PlotOptionsController",
|
||||
"./src/services/ExportImageService",
|
||||
"text!./res/templates/plot.html",
|
||||
"text!./res/templates/plot-options-browse.html",
|
||||
'legacyRegistry'
|
||||
], function (
|
||||
MCTChart,
|
||||
PlotController,
|
||||
PlotViewPolicy,
|
||||
PlotOptionsController,
|
||||
exportImageService,
|
||||
plotTemplate,
|
||||
plotOptionsBrowseTemplate,
|
||||
legacyRegistry
|
||||
) {
|
||||
|
||||
legacyRegistry.register("platform/features/plot", {
|
||||
"name": "Plot view for telemetry",
|
||||
"extensions": {
|
||||
"views": [
|
||||
{
|
||||
"name": "Plot",
|
||||
"key": "plot",
|
||||
"cssClass": "icon-sine",
|
||||
"template": plotTemplate,
|
||||
"needs": [
|
||||
"telemetry"
|
||||
],
|
||||
"priority": "preferred",
|
||||
"delegation": true
|
||||
}
|
||||
],
|
||||
"directives": [
|
||||
{
|
||||
"key": "mctChart",
|
||||
"implementation": MCTChart,
|
||||
"depends": [
|
||||
"$interval",
|
||||
"$log"
|
||||
]
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
{
|
||||
"key": "PlotController",
|
||||
"implementation": PlotController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"$element",
|
||||
"exportImageService",
|
||||
"telemetryFormatter",
|
||||
"telemetryHandler",
|
||||
"throttle",
|
||||
"PLOT_FIXED_DURATION",
|
||||
"openmct"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "PlotOptionsController",
|
||||
"implementation": PlotOptionsController,
|
||||
"depends": [
|
||||
"$scope"
|
||||
]
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"key": "exportImageService",
|
||||
"implementation": exportImageService,
|
||||
"depends": [
|
||||
"$q",
|
||||
"$timeout",
|
||||
"$log",
|
||||
"EXPORT_IMAGE_TIMEOUT"
|
||||
]
|
||||
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
{
|
||||
"key": "PLOT_FIXED_DURATION",
|
||||
"value": 900000,
|
||||
"priority": "fallback",
|
||||
"comment": "Fifteen minutes."
|
||||
},
|
||||
{
|
||||
"key": "EXPORT_IMAGE_TIMEOUT",
|
||||
"value": 500,
|
||||
"priority": "fallback"
|
||||
}
|
||||
],
|
||||
"policies": [
|
||||
{
|
||||
"category": "view",
|
||||
"implementation": PlotViewPolicy,
|
||||
"depends": [
|
||||
"openmct"
|
||||
]
|
||||
}
|
||||
],
|
||||
"representations": [
|
||||
{
|
||||
"key": "plot-options-browse",
|
||||
"template": plotOptionsBrowseTemplate
|
||||
}
|
||||
],
|
||||
"licenses": [
|
||||
{
|
||||
"name": "FileSaver.js",
|
||||
"version": "0.0.2",
|
||||
"author": "Eli Grey",
|
||||
"description": "File download initiator (for file exports)",
|
||||
"website": "https://github.com/eligrey/FileSaver.js/",
|
||||
"copyright": "Copyright © 2015 Eli Grey.",
|
||||
"license": "license-mit",
|
||||
"link": "https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md"
|
||||
},
|
||||
{
|
||||
"name": "html2canvas",
|
||||
"version": "0.4.1",
|
||||
"author": "Niklas von Hertzen",
|
||||
"description": "JavaScript HTML renderer",
|
||||
"website": "https://github.com/niklasvh/html2canvas",
|
||||
"copyright": "Copyright © 2012 Niklas von Hertzen.",
|
||||
"license": "license-mit",
|
||||
"link": "https://github.com/niklasvh/html2canvas/blob/master/LICENSE"
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
@ -1,70 +0,0 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2017, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div ng-controller="PlotOptionsController" class="flex-elem grows l-inspector-part">
|
||||
<em class="t-inspector-part-header" title="Display properties for this object">Plot Options</em>
|
||||
<mct-form
|
||||
ng-model="configuration.plot.xAxis"
|
||||
structure="xAxisForm"
|
||||
name="xAxisFormState"
|
||||
class="flex-elem l-flex-row no-margin">
|
||||
</mct-form>
|
||||
<mct-form
|
||||
ng-model="configuration.plot.yAxis"
|
||||
structure="yAxisForm"
|
||||
name="yAxisFormState"
|
||||
class="flex-elem l-flex-row no-margin">
|
||||
</mct-form>
|
||||
<div class="form">
|
||||
<div class="section-header ng-binding ng-scope">
|
||||
Plot Series
|
||||
</div>
|
||||
<ul class="first flex-elem grows vscroll">
|
||||
<ul class="tree">
|
||||
<li ng-repeat="child in children">
|
||||
<span ng-controller="ToggleController as toggle">
|
||||
<span ng-controller="TreeNodeController as treeNode">
|
||||
<span class="tree-item menus-to-left">
|
||||
<span
|
||||
class='ui-symbol view-control flex-elem has-children'
|
||||
ng-class="{ expanded: toggle.isActive() }"
|
||||
ng-click="toggle.toggle(); treeNode.trackExpansion()">
|
||||
</span>
|
||||
<mct-representation
|
||||
class="rep-object-label"
|
||||
key="'label'"
|
||||
mct-object="child">
|
||||
</mct-representation>
|
||||
</span>
|
||||
</span>
|
||||
<mct-form
|
||||
ng-class="{hidden: !toggle.isActive()}"
|
||||
ng-model="configuration.plot.series[$index]"
|
||||
structure="plotSeriesForm"
|
||||
name="plotOptionsState"
|
||||
class="flex-elem l-flex-row">
|
||||
</mct-form>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user