Compare commits
14 Commits
omm-large-
...
mct6113-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
911bb0b8e9 | ||
|
|
ce567c387c | ||
|
|
55d125d429 | ||
|
|
ab3c52c9b2 | ||
|
|
f354e1d263 | ||
|
|
7ff366c342 | ||
|
|
fe38f17069 | ||
|
|
38c5054e30 | ||
|
|
18bacfd251 | ||
|
|
7ecfebb352 | ||
|
|
c7fb6079ef | ||
|
|
6193f79ce0 | ||
|
|
3422853d3d | ||
|
|
bc550d366b |
@@ -165,11 +165,11 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
|
|||||||
// in the correct location, such as a folder, layout, or plot.
|
// in the correct location, such as a folder, layout, or plot.
|
||||||
await page.goto(`${parentUrl}?hideTree=true`);
|
await page.goto(`${parentUrl}?hideTree=true`);
|
||||||
|
|
||||||
//Click the Create button
|
// Click the Create button
|
||||||
await page.click('button:has-text("Create")');
|
await page.click('button:has-text("Create")');
|
||||||
|
|
||||||
// Click 'Plan' menu option
|
// Click 'Gantt Chart' menu option
|
||||||
await page.click(`li:text("Plan")`);
|
await page.click(`li:text("Gantt Chart")`);
|
||||||
|
|
||||||
// Modify the name input field of the domain object to accept 'name'
|
// Modify the name input field of the domain object to accept 'name'
|
||||||
if (name) {
|
if (name) {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const testPlan = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
test.describe("Plan", () => {
|
test.describe("Plan", () => {
|
||||||
test("Create a Plan and display all plan events @unstable", async ({ page }) => {
|
test("Create a Plan and display all plan events", async ({ page }) => {
|
||||||
await page.goto('./', { waitUntil: 'networkidle' });
|
await page.goto('./', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
const plan = await createPlanFromJSON(page, {
|
const plan = await createPlanFromJSON(page, {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function (
|
|||||||
StatusAPI: StatusAPI.default,
|
StatusAPI: StatusAPI.default,
|
||||||
TelemetryAPI: TelemetryAPI,
|
TelemetryAPI: TelemetryAPI,
|
||||||
TimeAPI: TimeAPI.default,
|
TimeAPI: TimeAPI.default,
|
||||||
TypeRegistry: TypeRegistry,
|
TypeRegistry: TypeRegistry.default,
|
||||||
UserAPI: UserAPI.default,
|
UserAPI: UserAPI.default,
|
||||||
AnnotationAPI: AnnotationAPI.default
|
AnnotationAPI: AnnotationAPI.default
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,63 +20,25 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define(function () {
|
/**
|
||||||
|
* A Type describes a kind of domain object that may appear or be
|
||||||
/**
|
* created within Open MCT.
|
||||||
* A Type describes a kind of domain object that may appear or be
|
*
|
||||||
* created within Open MCT.
|
* @param {module:opemct.TypeRegistry~TypeDefinition} definition
|
||||||
*
|
* @class Type
|
||||||
* @param {module:opemct.TypeRegistry~TypeDefinition} definition
|
* @memberof module:openmct
|
||||||
* @class Type
|
*/
|
||||||
* @memberof module:openmct
|
export default class Type {
|
||||||
*/
|
constructor(definition) {
|
||||||
function Type(definition) {
|
|
||||||
this.definition = definition;
|
this.definition = definition;
|
||||||
if (definition.key) {
|
if (definition.key) {
|
||||||
this.key = definition.key;
|
this.key = definition.key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a domain object is an instance of this type.
|
|
||||||
* @param domainObject
|
|
||||||
* @returns {boolean} true if the domain object is of this type
|
|
||||||
* @memberof module:openmct.Type#
|
|
||||||
* @method check
|
|
||||||
*/
|
|
||||||
Type.prototype.check = function (domainObject) {
|
|
||||||
// Depends on assignment from MCT.
|
|
||||||
return domainObject.type === this.key;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a definition for this type that can be registered using the
|
|
||||||
* legacy bundle format.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Type.prototype.toLegacyDefinition = function () {
|
|
||||||
const def = {};
|
|
||||||
def.name = this.definition.name;
|
|
||||||
def.cssClass = this.definition.cssClass;
|
|
||||||
def.description = this.definition.description;
|
|
||||||
def.properties = this.definition.form;
|
|
||||||
|
|
||||||
if (this.definition.initialize) {
|
|
||||||
def.model = {};
|
|
||||||
this.definition.initialize(def.model);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.definition.creatable) {
|
|
||||||
def.features = ['creation'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return def;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a type definition from a legacy definition.
|
* Create a type definition from a legacy definition.
|
||||||
*/
|
*/
|
||||||
Type.definitionFromLegacyDefinition = function (legacyDefinition) {
|
static definitionFromLegacyDefinition(legacyDefinition) {
|
||||||
let definition = {};
|
let definition = {};
|
||||||
definition.name = legacyDefinition.name;
|
definition.name = legacyDefinition.name;
|
||||||
definition.cssClass = legacyDefinition.cssClass;
|
definition.cssClass = legacyDefinition.cssClass;
|
||||||
@@ -121,7 +83,39 @@ define(function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return definition;
|
return definition;
|
||||||
};
|
}
|
||||||
|
/**
|
||||||
|
* Check if a domain object is an instance of this type.
|
||||||
|
* @param domainObject
|
||||||
|
* @returns {boolean} true if the domain object is of this type
|
||||||
|
* @memberof module:openmct.Type#
|
||||||
|
* @method check
|
||||||
|
*/
|
||||||
|
check(domainObject) {
|
||||||
|
// Depends on assignment from MCT.
|
||||||
|
return domainObject.type === this.key;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get a definition for this type that can be registered using the
|
||||||
|
* legacy bundle format.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
toLegacyDefinition() {
|
||||||
|
const def = {};
|
||||||
|
def.name = this.definition.name;
|
||||||
|
def.cssClass = this.definition.cssClass;
|
||||||
|
def.description = this.definition.description;
|
||||||
|
def.properties = this.definition.form;
|
||||||
|
|
||||||
return Type;
|
if (this.definition.initialize) {
|
||||||
});
|
def.model = {};
|
||||||
|
this.definition.initialize(def.model);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.definition.creatable) {
|
||||||
|
def.features = ['creation'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,35 +19,36 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
define(['./Type'], function (Type) {
|
import Type from './Type';
|
||||||
const UNKNOWN_TYPE = new Type({
|
|
||||||
key: "unknown",
|
|
||||||
name: "Unknown Type",
|
|
||||||
cssClass: "icon-object-unknown"
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
const UNKNOWN_TYPE = new Type({
|
||||||
* @typedef TypeDefinition
|
key: "unknown",
|
||||||
* @memberof module:openmct.TypeRegistry~
|
name: "Unknown Type",
|
||||||
* @property {string} label the name for this type of object
|
cssClass: "icon-object-unknown"
|
||||||
* @property {string} description a longer-form description of this type
|
});
|
||||||
* @property {function (object)} [initialize] a function which initializes
|
|
||||||
* the model for new domain objects of this type
|
|
||||||
* @property {boolean} [creatable] true if users should be allowed to
|
|
||||||
* create this type (default: false)
|
|
||||||
* @property {string} [cssClass] the CSS class to apply for icons
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A TypeRegistry maintains the definitions for different types
|
* @typedef TypeDefinition
|
||||||
* that domain objects may have.
|
* @memberof module:openmct.TypeRegistry~
|
||||||
* @interface TypeRegistry
|
* @property {string} label the name for this type of object
|
||||||
* @memberof module:openmct
|
* @property {string} description a longer-form description of this type
|
||||||
*/
|
* @property {function (object)} [initialize] a function which initializes
|
||||||
function TypeRegistry() {
|
* the model for new domain objects of this type
|
||||||
|
* @property {boolean} [creatable] true if users should be allowed to
|
||||||
|
* create this type (default: false)
|
||||||
|
* @property {string} [cssClass] the CSS class to apply for icons
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A TypeRegistry maintains the definitions for different types
|
||||||
|
* that domain objects may have.
|
||||||
|
* @interface TypeRegistry
|
||||||
|
* @memberof module:openmct
|
||||||
|
*/
|
||||||
|
export default class TypeRegistry {
|
||||||
|
constructor() {
|
||||||
this.types = {};
|
this.types = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new object type.
|
* Register a new object type.
|
||||||
*
|
*
|
||||||
@@ -56,17 +57,16 @@ define(['./Type'], function (Type) {
|
|||||||
* @method addType
|
* @method addType
|
||||||
* @memberof module:openmct.TypeRegistry#
|
* @memberof module:openmct.TypeRegistry#
|
||||||
*/
|
*/
|
||||||
TypeRegistry.prototype.addType = function (typeKey, typeDef) {
|
addType(typeKey, typeDef) {
|
||||||
this.standardizeType(typeDef);
|
this.standardizeType(typeDef);
|
||||||
this.types[typeKey] = new Type(typeDef);
|
this.types[typeKey] = new Type(typeDef);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a typeDef, standardizes it, and logs warnings about unsupported
|
* Takes a typeDef, standardizes it, and logs warnings about unsupported
|
||||||
* usage.
|
* usage.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
TypeRegistry.prototype.standardizeType = function (typeDef) {
|
standardizeType(typeDef) {
|
||||||
if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) {
|
if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) {
|
||||||
if (!typeDef.name) {
|
if (!typeDef.name) {
|
||||||
typeDef.name = typeDef.label;
|
typeDef.name = typeDef.label;
|
||||||
@@ -74,18 +74,16 @@ define(['./Type'], function (Type) {
|
|||||||
|
|
||||||
delete typeDef.label;
|
delete typeDef.label;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List keys for all registered types.
|
* List keys for all registered types.
|
||||||
* @method listKeys
|
* @method listKeys
|
||||||
* @memberof module:openmct.TypeRegistry#
|
* @memberof module:openmct.TypeRegistry#
|
||||||
* @returns {string[]} all registered type keys
|
* @returns {string[]} all registered type keys
|
||||||
*/
|
*/
|
||||||
TypeRegistry.prototype.listKeys = function () {
|
listKeys() {
|
||||||
return Object.keys(this.types);
|
return Object.keys(this.types);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a registered type by its key.
|
* Retrieve a registered type by its key.
|
||||||
* @method get
|
* @method get
|
||||||
@@ -93,18 +91,15 @@ define(['./Type'], function (Type) {
|
|||||||
* @memberof module:openmct.TypeRegistry#
|
* @memberof module:openmct.TypeRegistry#
|
||||||
* @returns {module:openmct.Type} the registered type
|
* @returns {module:openmct.Type} the registered type
|
||||||
*/
|
*/
|
||||||
TypeRegistry.prototype.get = function (typeKey) {
|
get(typeKey) {
|
||||||
return this.types[typeKey] || UNKNOWN_TYPE;
|
return this.types[typeKey] || UNKNOWN_TYPE;
|
||||||
};
|
}
|
||||||
|
importLegacyTypes(types) {
|
||||||
TypeRegistry.prototype.importLegacyTypes = function (types) {
|
|
||||||
types.filter((t) => this.get(t.key) === UNKNOWN_TYPE)
|
types.filter((t) => this.get(t.key) === UNKNOWN_TYPE)
|
||||||
.forEach((type) => {
|
.forEach((type) => {
|
||||||
let def = Type.definitionFromLegacyDefinition(type);
|
let def = Type.definitionFromLegacyDefinition(type);
|
||||||
this.addType(type.key, def);
|
this.addType(type.key, def);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
}
|
||||||
return TypeRegistry;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,36 +20,36 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define(['./TypeRegistry', './Type'], function (TypeRegistry, Type) {
|
import TypeRegistry from './TypeRegistry';
|
||||||
describe('The Type API', function () {
|
|
||||||
let typeRegistryInstance;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
describe('The Type API', function () {
|
||||||
typeRegistryInstance = new TypeRegistry ();
|
let typeRegistryInstance;
|
||||||
typeRegistryInstance.addType('testType', {
|
|
||||||
name: 'Test Type',
|
|
||||||
description: 'This is a test type.',
|
|
||||||
creatable: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('types can be standardized', function () {
|
beforeEach(function () {
|
||||||
typeRegistryInstance.addType('standardizationTestType', {
|
typeRegistryInstance = new TypeRegistry ();
|
||||||
label: 'Test Type',
|
typeRegistryInstance.addType('testType', {
|
||||||
description: 'This is a test type.',
|
name: 'Test Type',
|
||||||
creatable: true
|
description: 'This is a test type.',
|
||||||
});
|
creatable: true
|
||||||
typeRegistryInstance.standardizeType(typeRegistryInstance.types.standardizationTestType);
|
|
||||||
expect(typeRegistryInstance.get('standardizationTestType').definition.label).toBeUndefined();
|
|
||||||
expect(typeRegistryInstance.get('standardizationTestType').definition.name).toBe('Test Type');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('new types are registered successfully and can be retrieved', function () {
|
|
||||||
expect(typeRegistryInstance.get('testType').definition.name).toBe('Test Type');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('type registry contains new keys', function () {
|
|
||||||
expect(typeRegistryInstance.listKeys ()).toContain('testType');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('types can be standardized', function () {
|
||||||
|
typeRegistryInstance.addType('standardizationTestType', {
|
||||||
|
label: 'Test Type',
|
||||||
|
description: 'This is a test type.',
|
||||||
|
creatable: true
|
||||||
|
});
|
||||||
|
typeRegistryInstance.standardizeType(typeRegistryInstance.types.standardizationTestType);
|
||||||
|
expect(typeRegistryInstance.get('standardizationTestType').definition.label).toBeUndefined();
|
||||||
|
expect(typeRegistryInstance.get('standardizationTestType').definition.name).toBe('Test Type');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('new types are registered successfully and can be retrieved', function () {
|
||||||
|
expect(typeRegistryInstance.get('testType').definition.name).toBe('Test Type');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('type registry contains new keys', function () {
|
||||||
|
expect(typeRegistryInstance.listKeys ()).toContain('testType');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ export default function BarGraphInspectorViewProvider(openmct) {
|
|||||||
template: '<bar-graph-options></bar-graph-options>'
|
template: '<bar-graph-options></bar-graph-options>'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
priority: function () {
|
||||||
|
return openmct.priority.HIGH + 1;
|
||||||
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (component) {
|
if (component) {
|
||||||
component.$destroy();
|
component.$destroy();
|
||||||
@@ -40,9 +43,6 @@ export default function BarGraphInspectorViewProvider(openmct) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
|
||||||
priority: function () {
|
|
||||||
return openmct.priority.HIGH + 1;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ export default function ScatterPlotInspectorViewProvider(openmct) {
|
|||||||
template: '<plot-options></plot-options>'
|
template: '<plot-options></plot-options>'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
priority: function () {
|
||||||
|
return openmct.priority.HIGH + 1;
|
||||||
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (component) {
|
if (component) {
|
||||||
component.$destroy();
|
component.$destroy();
|
||||||
@@ -40,9 +43,6 @@ export default function ScatterPlotInspectorViewProvider(openmct) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
|
||||||
priority: function () {
|
|
||||||
return openmct.priority.HIGH + 1;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ export default function FaultManagementInspectorViewProvider(openmct) {
|
|||||||
template: '<FaultManagementInspector></FaultManagementInspector>'
|
template: '<FaultManagementInspector></FaultManagementInspector>'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
priority: function () {
|
||||||
|
return openmct.priority.HIGH + 1;
|
||||||
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (component) {
|
if (component) {
|
||||||
component.$destroy();
|
component.$destroy();
|
||||||
@@ -63,9 +66,6 @@ export default function FaultManagementInspectorViewProvider(openmct) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
|
||||||
priority: function () {
|
|
||||||
return openmct.priority.HIGH + 1;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ define([
|
|||||||
|
|
||||||
return hasPersistedFilters || hasGlobalFilters;
|
return hasPersistedFilters || hasGlobalFilters;
|
||||||
},
|
},
|
||||||
|
priority: function () {
|
||||||
|
return openmct.priority.DEFAULT;
|
||||||
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (component) {
|
if (component) {
|
||||||
component.$destroy();
|
component.$destroy();
|
||||||
@@ -68,9 +71,6 @@ define([
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
|
||||||
priority: function () {
|
|
||||||
return openmct.priority.DEFAULT;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,14 +76,14 @@ export default function StylesInspectorViewProvider(openmct) {
|
|||||||
template: `<StylesInspectorView />`
|
template: `<StylesInspectorView />`
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
priority: function () {
|
||||||
|
return openmct.priority.DEFAULT;
|
||||||
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
component.$destroy();
|
component.$destroy();
|
||||||
component = undefined;
|
component = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
|
||||||
priority: function () {
|
|
||||||
return this.openmct.priority.DEFAULT;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/plugins/plan/GanttChartCompositionPolicy.js
Normal file
35
src/plugins/plan/GanttChartCompositionPolicy.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2023, 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
const ALLOWED_TYPES = [
|
||||||
|
'plan'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ganttChartCompositionPolicy(openmct) {
|
||||||
|
return function (parent, child) {
|
||||||
|
if (parent.type === 'gantt-chart') {
|
||||||
|
return ALLOWED_TYPES.includes(child.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -51,6 +51,7 @@ import TimelineAxis from "../../ui/components/TimeSystemAxis.vue";
|
|||||||
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
|
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
|
||||||
import { getValidatedData } from "./util";
|
import { getValidatedData } from "./util";
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
const PADDING = 1;
|
const PADDING = 1;
|
||||||
const OUTER_TEXT_PADDING = 12;
|
const OUTER_TEXT_PADDING = 12;
|
||||||
@@ -61,15 +62,16 @@ const RESIZE_POLL_INTERVAL = 200;
|
|||||||
const ROW_HEIGHT = 25;
|
const ROW_HEIGHT = 25;
|
||||||
const LINE_HEIGHT = 12;
|
const LINE_HEIGHT = 12;
|
||||||
const MAX_TEXT_WIDTH = 300;
|
const MAX_TEXT_WIDTH = 300;
|
||||||
const EDGE_ROUNDING = 5;
|
const MIN_ACTIVITY_WIDTH = 2;
|
||||||
const DEFAULT_COLOR = '#cc9922';
|
const DEFAULT_COLOR = '#cc9922';
|
||||||
|
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
TimelineAxis,
|
TimelineAxis,
|
||||||
SwimLane
|
SwimLane
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'domainObject', 'path'],
|
inject: ['openmct', 'domainObject', 'path', 'composition'],
|
||||||
props: {
|
props: {
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -88,8 +90,9 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
viewBounds: undefined,
|
viewBounds: null,
|
||||||
timeSystem: undefined,
|
timeSystem: null,
|
||||||
|
clipActivityNames: this.domainObject?.configuration?.clipActivityNames ?? false,
|
||||||
height: 0
|
height: 0
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -103,9 +106,10 @@ export default {
|
|||||||
this.setDimensions();
|
this.setDimensions();
|
||||||
this.setTimeContext();
|
this.setTimeContext();
|
||||||
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||||
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
|
|
||||||
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
|
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
|
||||||
this.status = this.openmct.status.get(this.domainObject.identifier);
|
this.status = this.openmct.status.get(this.domainObject.identifier);
|
||||||
|
this.stopObservingConfig = this.openmct.objects.observe(this.domainObject, 'configuration', this.handleConfigurationChange);
|
||||||
|
this.loadComposition();
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
clearInterval(this.resizeTimer);
|
clearInterval(this.resizeTimer);
|
||||||
@@ -117,8 +121,18 @@ export default {
|
|||||||
if (this.removeStatusListener) {
|
if (this.removeStatusListener) {
|
||||||
this.removeStatusListener();
|
this.removeStatusListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.composition) {
|
||||||
|
this.composition.off('add', this.handleCompositionAdd);
|
||||||
|
this.composition.off('remove', this.handleCompositionRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stopObservingConfig();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
activityNameFitsRect(activityName, rectWidth) {
|
||||||
|
return (this.getTextWidth(activityName) + TEXT_LEFT_PADDING) < rectWidth;
|
||||||
|
},
|
||||||
setTimeContext() {
|
setTimeContext() {
|
||||||
this.stopFollowingTimeContext();
|
this.stopFollowingTimeContext();
|
||||||
this.timeContext = this.openmct.time.getContextForView(this.path);
|
this.timeContext = this.openmct.time.getContextForView(this.path);
|
||||||
@@ -131,6 +145,14 @@ export default {
|
|||||||
this.timeContext.on("bounds", this.updateViewBounds);
|
this.timeContext.on("bounds", this.updateViewBounds);
|
||||||
this.timeContext.on("clock", this.updateBounds);
|
this.timeContext.on("clock", this.updateBounds);
|
||||||
},
|
},
|
||||||
|
loadComposition() {
|
||||||
|
if (this.composition) {
|
||||||
|
this.composition.on('add', this.handleCompositionAdd);
|
||||||
|
this.composition.on('remove', this.handleCompositionRemove);
|
||||||
|
this.composition.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
stopFollowingTimeContext() {
|
stopFollowingTimeContext() {
|
||||||
if (this.timeContext) {
|
if (this.timeContext) {
|
||||||
this.timeContext.off("timeSystem", this.setScaleAndPlotActivities);
|
this.timeContext.off("timeSystem", this.setScaleAndPlotActivities);
|
||||||
@@ -138,6 +160,63 @@ export default {
|
|||||||
this.timeContext.off("clock", this.updateBounds);
|
this.timeContext.off("clock", this.updateBounds);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
showReplacePlanDialog(domainObject) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let dialog = this.openmct.overlays.dialog({
|
||||||
|
iconClass: 'alert',
|
||||||
|
message: 'This action will replace the current Plan. Do you want to continue?',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
label: 'Ok',
|
||||||
|
emphasis: true,
|
||||||
|
callback: () => {
|
||||||
|
this.removeFromComposition(this.planObject);
|
||||||
|
this.planObject = domainObject;
|
||||||
|
this.getPlanData(domainObject);
|
||||||
|
this.setScaleAndPlotActivities();
|
||||||
|
resolve();
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cancel',
|
||||||
|
callback: () => {
|
||||||
|
this.removeFromComposition(domainObject);
|
||||||
|
resolve();
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
async handleCompositionAdd(domainObject) {
|
||||||
|
if (this.planObject) {
|
||||||
|
await this.showReplacePlanDialog(domainObject);
|
||||||
|
} else {
|
||||||
|
this.planObject = domainObject;
|
||||||
|
this.getPlanData(domainObject);
|
||||||
|
this.setScaleAndPlotActivities();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleConfigurationChange(newConfiguration) {
|
||||||
|
Object.keys(newConfiguration).forEach((key) => {
|
||||||
|
this[key] = newConfiguration[key];
|
||||||
|
});
|
||||||
|
this.setScaleAndPlotActivities();
|
||||||
|
},
|
||||||
|
handleCompositionRemove(identifier) {
|
||||||
|
if (this.planObject && this.openmct.objects.areIdsEqual(identifier, this.planObject?.identifier)) {
|
||||||
|
this.planObject = null;
|
||||||
|
this.planData = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setScaleAndPlotActivities();
|
||||||
|
},
|
||||||
|
removeFromComposition(domainObject) {
|
||||||
|
this.composition.remove(domainObject);
|
||||||
|
},
|
||||||
observeForChanges(mutatedObject) {
|
observeForChanges(mutatedObject) {
|
||||||
this.getPlanData(mutatedObject);
|
this.getPlanData(mutatedObject);
|
||||||
this.setScaleAndPlotActivities();
|
this.setScaleAndPlotActivities();
|
||||||
@@ -193,14 +272,14 @@ export default {
|
|||||||
this.viewBounds = Object.create(bounds);
|
this.viewBounds = Object.create(bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.timeSystem === undefined) {
|
if (this.timeSystem === null) {
|
||||||
this.timeSystem = this.openmct.time.timeSystem();
|
this.timeSystem = this.openmct.time.timeSystem();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setScaleAndPlotActivities();
|
this.setScaleAndPlotActivities();
|
||||||
},
|
},
|
||||||
setScaleAndPlotActivities(timeSystem) {
|
setScaleAndPlotActivities(timeSystem) {
|
||||||
if (timeSystem !== undefined) {
|
if (timeSystem) {
|
||||||
this.timeSystem = timeSystem;
|
this.timeSystem = timeSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +303,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeSystem === undefined) {
|
if (!timeSystem) {
|
||||||
timeSystem = this.openmct.time.timeSystem();
|
timeSystem = this.openmct.time.timeSystem();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,10 +324,15 @@ export default {
|
|||||||
isActivityInBounds(activity) {
|
isActivityInBounds(activity) {
|
||||||
return (activity.start < this.viewBounds.end) && (activity.end > this.viewBounds.start);
|
return (activity.start < this.viewBounds.end) && (activity.end > this.viewBounds.start);
|
||||||
},
|
},
|
||||||
getTextWidth(name) {
|
/**
|
||||||
let metrics = this.canvasContext.measureText(name);
|
* Get the width of the given text in pixels.
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {number} width of the text in pixels (as a double)
|
||||||
|
*/
|
||||||
|
getTextWidth(text) {
|
||||||
|
const textMetrics = this.canvasContext.measureText(text);
|
||||||
|
|
||||||
return parseInt(metrics.width, 10);
|
return textMetrics.width;
|
||||||
},
|
},
|
||||||
sortFn(a, b) {
|
sortFn(a, b) {
|
||||||
const numA = parseInt(a, 10);
|
const numA = parseInt(a, 10);
|
||||||
@@ -303,61 +387,62 @@ export default {
|
|||||||
|
|
||||||
let activities = this.planData[key];
|
let activities = this.planData[key];
|
||||||
activities.forEach((activity) => {
|
activities.forEach((activity) => {
|
||||||
if (this.isActivityInBounds(activity)) {
|
if (!this.isActivityInBounds(activity)) {
|
||||||
const currentStart = Math.max(this.viewBounds.start, activity.start);
|
return;
|
||||||
const currentEnd = Math.min(this.viewBounds.end, activity.end);
|
|
||||||
const rectX = this.xScale(currentStart);
|
|
||||||
const rectY = this.xScale(currentEnd);
|
|
||||||
const rectWidth = rectY - rectX;
|
|
||||||
|
|
||||||
const activityNameWidth = this.getTextWidth(activity.name) + TEXT_LEFT_PADDING;
|
|
||||||
//TODO: Fix bug for SVG where the rectWidth is not proportional to the canvas measuredWidth of the text
|
|
||||||
const activityNameFitsRect = (rectWidth >= activityNameWidth);
|
|
||||||
const textStart = (activityNameFitsRect ? rectX : rectY) + TEXT_LEFT_PADDING;
|
|
||||||
const color = activity.color || DEFAULT_COLOR;
|
|
||||||
let textColor = '';
|
|
||||||
if (activity.textColor) {
|
|
||||||
textColor = activity.textColor;
|
|
||||||
} else if (activityNameFitsRect) {
|
|
||||||
textColor = this.getContrastingColor(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
let textLines = this.getActivityDisplayText(this.canvasContext, activity.name, activityNameFitsRect);
|
|
||||||
const textWidth = textStart + this.getTextWidth(textLines[0]) + TEXT_LEFT_PADDING;
|
|
||||||
|
|
||||||
if (activityNameFitsRect) {
|
|
||||||
currentRow = this.getRowForActivity(rectX, rectWidth, activitiesByRow);
|
|
||||||
} else {
|
|
||||||
currentRow = this.getRowForActivity(rectX, textWidth, activitiesByRow);
|
|
||||||
}
|
|
||||||
|
|
||||||
let textY = parseInt(currentRow, 10) + (activityNameFitsRect ? INNER_TEXT_PADDING : OUTER_TEXT_PADDING);
|
|
||||||
|
|
||||||
if (!activitiesByRow[currentRow]) {
|
|
||||||
activitiesByRow[currentRow] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
activitiesByRow[currentRow].push({
|
|
||||||
activity: {
|
|
||||||
color: color,
|
|
||||||
textColor: textColor,
|
|
||||||
name: activity.name,
|
|
||||||
exceeds: {
|
|
||||||
start: this.xScale(this.viewBounds.start) > this.xScale(activity.start),
|
|
||||||
end: this.xScale(this.viewBounds.end) < this.xScale(activity.end)
|
|
||||||
},
|
|
||||||
start: activity.start,
|
|
||||||
end: activity.end
|
|
||||||
},
|
|
||||||
textLines: textLines,
|
|
||||||
textStart: textStart,
|
|
||||||
textClass: activityNameFitsRect ? "" : "activity-label--outside-rect",
|
|
||||||
textY: textY,
|
|
||||||
start: rectX,
|
|
||||||
end: activityNameFitsRect ? rectY : textStart + textWidth,
|
|
||||||
rectWidth: rectWidth
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentStart = Math.max(this.viewBounds.start, activity.start);
|
||||||
|
const currentEnd = Math.min(this.viewBounds.end, activity.end);
|
||||||
|
const rectX1 = this.xScale(currentStart);
|
||||||
|
const rectX2 = this.xScale(currentEnd);
|
||||||
|
const rectWidth = rectX2 - rectX1;
|
||||||
|
|
||||||
|
//TODO: Fix bug for SVG where the rectWidth is not proportional to the canvas measuredWidth of the text
|
||||||
|
const showTextInsideRect = this.clipActivityNames || this.activityNameFitsRect(activity.name, rectWidth);
|
||||||
|
const textStart = (showTextInsideRect ? rectX1 : rectX2) + TEXT_LEFT_PADDING;
|
||||||
|
const color = activity.color || DEFAULT_COLOR;
|
||||||
|
let textColor = '';
|
||||||
|
if (activity.textColor) {
|
||||||
|
textColor = activity.textColor;
|
||||||
|
} else if (showTextInsideRect) {
|
||||||
|
textColor = this.getContrastingColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
const textLines = this.getActivityDisplayText(this.canvasContext, activity.name, showTextInsideRect);
|
||||||
|
const textWidth = textStart + this.getTextWidth(textLines[0]) + TEXT_LEFT_PADDING;
|
||||||
|
|
||||||
|
if (showTextInsideRect) {
|
||||||
|
currentRow = this.getRowForActivity(rectX1, rectWidth, activitiesByRow);
|
||||||
|
} else {
|
||||||
|
currentRow = this.getRowForActivity(rectX1, textWidth, activitiesByRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
let textY = parseInt(currentRow, 10) + (showTextInsideRect ? INNER_TEXT_PADDING : OUTER_TEXT_PADDING);
|
||||||
|
|
||||||
|
if (!activitiesByRow[currentRow]) {
|
||||||
|
activitiesByRow[currentRow] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
activitiesByRow[currentRow].push({
|
||||||
|
activity: {
|
||||||
|
color: color,
|
||||||
|
textColor: textColor,
|
||||||
|
name: activity.name,
|
||||||
|
exceeds: {
|
||||||
|
start: this.xScale(this.viewBounds.start) > this.xScale(activity.start),
|
||||||
|
end: this.xScale(this.viewBounds.end) < this.xScale(activity.end)
|
||||||
|
},
|
||||||
|
start: activity.start,
|
||||||
|
end: activity.end
|
||||||
|
},
|
||||||
|
textLines: textLines,
|
||||||
|
textStart: textStart,
|
||||||
|
textClass: showTextInsideRect ? "" : "activity-label--outside-rect",
|
||||||
|
textY: textY,
|
||||||
|
start: rectX1,
|
||||||
|
end: showTextInsideRect ? rectX2 : textStart + textWidth,
|
||||||
|
rectWidth: rectWidth
|
||||||
|
});
|
||||||
});
|
});
|
||||||
this.groupActivities[key] = {
|
this.groupActivities[key] = {
|
||||||
heading: key,
|
heading: key,
|
||||||
@@ -365,28 +450,32 @@ export default {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getActivityDisplayText(context, text, activityNameFitsRect) {
|
/**
|
||||||
//TODO: If the activity start is less than viewBounds.start then the text should be cropped on the left/should be off-screen)
|
* Format the activity name to fit within the activity rect with a max of 2 lines
|
||||||
let words = text.split(' ');
|
* @param {CanvasRenderingContext2D} canvasContext
|
||||||
|
* @param {string} activityName
|
||||||
|
* @param {boolean} activityNameFitsRect
|
||||||
|
*/
|
||||||
|
getActivityDisplayText(canvasContext, activityName, activityNameFitsRect) {
|
||||||
|
// TODO: If the activity start is less than viewBounds.start then the text should be cropped on the left/should be off-screen)
|
||||||
|
let words = activityName.split(' ');
|
||||||
let line = '';
|
let line = '';
|
||||||
let activityText = [];
|
let activityLines = [];
|
||||||
let rows = 1;
|
|
||||||
|
|
||||||
for (let n = 0; (n < words.length) && (rows <= 2); n++) {
|
for (let n = 0; (n < words.length) && (activityLines.length <= 2); n++) {
|
||||||
let testLine = line + words[n] + ' ';
|
let tempLine = line + words[n] + ' ';
|
||||||
let metrics = context.measureText(testLine);
|
let textMetrics = canvasContext.measureText(tempLine);
|
||||||
let testWidth = metrics.width;
|
const textWidth = textMetrics.width;
|
||||||
if (!activityNameFitsRect && (testWidth > MAX_TEXT_WIDTH && n > 0)) {
|
if (!activityNameFitsRect && (textWidth > MAX_TEXT_WIDTH && n > 0)) {
|
||||||
activityText.push(line);
|
activityLines.push(line);
|
||||||
line = words[n] + ' ';
|
line = words[n] + ' ';
|
||||||
testLine = line + words[n] + ' ';
|
tempLine = line + words[n] + ' ';
|
||||||
rows = rows + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
line = testLine;
|
line = tempLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
return activityText.length ? activityText : [line];
|
return activityLines.length ? activityLines : [line];
|
||||||
},
|
},
|
||||||
getGroupContainer(activityRows, heading) {
|
getGroupContainer(activityRows, heading) {
|
||||||
let svgHeight = 30;
|
let svgHeight = 30;
|
||||||
@@ -418,7 +507,24 @@ export default {
|
|||||||
width: svgWidth
|
width: svgWidth
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
template: `<swim-lane :is-nested="isNested" :status="status"><template slot="label">{{heading}}</template><template slot="object"><svg :height="height" :width="width"></svg></template></swim-lane>`
|
template: `
|
||||||
|
<swim-lane
|
||||||
|
:is-nested="isNested"
|
||||||
|
:status="status"
|
||||||
|
>
|
||||||
|
<template slot="label">
|
||||||
|
{{heading}}
|
||||||
|
</template>
|
||||||
|
<template slot="object">
|
||||||
|
<svg
|
||||||
|
:height="height"
|
||||||
|
:width="width"
|
||||||
|
:viewBox="'0 0 ' + width + ' ' + height"
|
||||||
|
>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
</swim-lane>
|
||||||
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$refs.planHolder.appendChild(component.$mount().$el);
|
this.$refs.planHolder.appendChild(component.$mount().$el);
|
||||||
@@ -432,7 +538,6 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
drawPlan() {
|
drawPlan() {
|
||||||
|
|
||||||
Object.keys(this.groupActivities).forEach((group, index) => {
|
Object.keys(this.groupActivities).forEach((group, index) => {
|
||||||
const activitiesByRow = this.groupActivities[group].activitiesByRow;
|
const activitiesByRow = this.groupActivities[group].activitiesByRow;
|
||||||
const heading = this.groupActivities[group].heading;
|
const heading = this.groupActivities[group].heading;
|
||||||
@@ -454,7 +559,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
plotNoItems(svgElement) {
|
plotNoItems(svgElement) {
|
||||||
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
const textElement = document.createElementNS(SVG_NAMESPACE, 'text');
|
||||||
this.setNSAttributesForElement(textElement, {
|
this.setNSAttributesForElement(textElement, {
|
||||||
x: "10",
|
x: "10",
|
||||||
y: "20",
|
y: "20",
|
||||||
@@ -464,6 +569,10 @@ export default {
|
|||||||
|
|
||||||
svgElement.appendChild(textElement);
|
svgElement.appendChild(textElement);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @param {Element} element
|
||||||
|
* @param {Object} attributes
|
||||||
|
*/
|
||||||
setNSAttributesForElement(element, attributes) {
|
setNSAttributesForElement(element, attributes) {
|
||||||
Object.keys(attributes).forEach((key) => {
|
Object.keys(attributes).forEach((key) => {
|
||||||
element.setAttributeNS(null, key, attributes[key]);
|
element.setAttributeNS(null, key, attributes[key]);
|
||||||
@@ -474,7 +583,7 @@ export default {
|
|||||||
},
|
},
|
||||||
// Experimental for now - unused
|
// Experimental for now - unused
|
||||||
addForeignElement(svgElement, label, x, y) {
|
addForeignElement(svgElement, label, x, y) {
|
||||||
let foreign = document.createElementNS('http://www.w3.org/2000/svg', "foreignObject");
|
let foreign = document.createElementNS(SVG_NAMESPACE, "foreignObject");
|
||||||
this.setNSAttributesForElement(foreign, {
|
this.setNSAttributesForElement(foreign, {
|
||||||
width: String(MAX_TEXT_WIDTH),
|
width: String(MAX_TEXT_WIDTH),
|
||||||
height: String(LINE_HEIGHT * 2),
|
height: String(LINE_HEIGHT * 2),
|
||||||
@@ -490,27 +599,33 @@ export default {
|
|||||||
|
|
||||||
svgElement.appendChild(foreign);
|
svgElement.appendChild(foreign);
|
||||||
},
|
},
|
||||||
|
// TODO: Clean up, extract HTML element creation into utility functions
|
||||||
plotActivity(item, row, svgElement) {
|
plotActivity(item, row, svgElement) {
|
||||||
const activity = item.activity;
|
const activity = item.activity;
|
||||||
let width = item.rectWidth;
|
const rectElement = document.createElementNS(SVG_NAMESPACE, 'rect');
|
||||||
let rectElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
const width = Math.max(Math.round(item.rectWidth), MIN_ACTIVITY_WIDTH);
|
||||||
|
const clipUuid = uuid();
|
||||||
|
|
||||||
if (item.activity.exceeds.start) {
|
if (this.clipActivityNames) {
|
||||||
width = width + EDGE_ROUNDING;
|
const clipPathElement = document.createElementNS(SVG_NAMESPACE, 'clipPath');
|
||||||
|
this.setNSAttributesForElement(clipPathElement, {
|
||||||
|
id: `clip-${clipUuid}`
|
||||||
|
});
|
||||||
|
svgElement.appendChild(clipPathElement);
|
||||||
|
let clipRectElement = document.createElementNS(SVG_NAMESPACE, 'rect');
|
||||||
|
this.setNSAttributesForElement(clipRectElement, {
|
||||||
|
x: Math.round(item.start),
|
||||||
|
y: row,
|
||||||
|
width: width,
|
||||||
|
height: String(ROW_HEIGHT)
|
||||||
|
});
|
||||||
|
clipPathElement.appendChild(clipRectElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.activity.exceeds.end) {
|
|
||||||
width = width + EDGE_ROUNDING;
|
|
||||||
}
|
|
||||||
|
|
||||||
width = Math.max(width, 1); // Set width to a minimum of 1
|
|
||||||
|
|
||||||
// rx: don't round corners if the width of the rect is smaller than the rounding radius
|
|
||||||
this.setNSAttributesForElement(rectElement, {
|
this.setNSAttributesForElement(rectElement, {
|
||||||
class: 'activity-bounds',
|
class: 'activity-bounds',
|
||||||
x: item.activity.exceeds.start ? item.start - EDGE_ROUNDING : item.start,
|
x: Math.round(item.start),
|
||||||
y: row,
|
y: row,
|
||||||
rx: (width < EDGE_ROUNDING * 2) ? 0 : EDGE_ROUNDING,
|
|
||||||
width: width,
|
width: width,
|
||||||
height: String(ROW_HEIGHT),
|
height: String(ROW_HEIGHT),
|
||||||
fill: activity.color
|
fill: activity.color
|
||||||
@@ -518,12 +633,13 @@ export default {
|
|||||||
|
|
||||||
rectElement.addEventListener('click', (event) => {
|
rectElement.addEventListener('click', (event) => {
|
||||||
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
|
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
|
||||||
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
svgElement.appendChild(rectElement);
|
svgElement.appendChild(rectElement);
|
||||||
|
|
||||||
item.textLines.forEach((line, index) => {
|
item.textLines.forEach((line, index) => {
|
||||||
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
let textElement = document.createElementNS(SVG_NAMESPACE, 'text');
|
||||||
this.setNSAttributesForElement(textElement, {
|
this.setNSAttributesForElement(textElement, {
|
||||||
class: `activity-label ${item.textClass}`,
|
class: `activity-label ${item.textClass}`,
|
||||||
x: item.textStart,
|
x: item.textStart,
|
||||||
@@ -531,10 +647,17 @@ export default {
|
|||||||
fill: activity.textColor
|
fill: activity.textColor
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.clipActivityNames) {
|
||||||
|
this.setNSAttributesForElement(textElement, {
|
||||||
|
'clip-path': `url(#clip-${clipUuid})`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const textNode = document.createTextNode(line);
|
const textNode = document.createTextNode(line);
|
||||||
textElement.appendChild(textNode);
|
textElement.appendChild(textNode);
|
||||||
textElement.addEventListener('click', (event) => {
|
textElement.addEventListener('click', (event) => {
|
||||||
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
|
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
|
||||||
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
svgElement.appendChild(textElement);
|
svgElement.appendChild(textElement);
|
||||||
});
|
});
|
||||||
@@ -566,6 +689,7 @@ export default {
|
|||||||
setSelectionForActivity(element, activity, multiSelect) {
|
setSelectionForActivity(element, activity, multiSelect) {
|
||||||
this.openmct.selection.select([{
|
this.openmct.selection.select([{
|
||||||
element: element,
|
element: element,
|
||||||
|
// activity: activity,
|
||||||
context: {
|
context: {
|
||||||
type: 'activity',
|
type: 'activity',
|
||||||
activity: activity
|
activity: activity
|
||||||
@@ -573,11 +697,11 @@ export default {
|
|||||||
}, {
|
}, {
|
||||||
element: this.openmct.layout.$refs.browseObject.$el,
|
element: this.openmct.layout.$refs.browseObject.$el,
|
||||||
context: {
|
context: {
|
||||||
|
// activity: activity,
|
||||||
item: this.domainObject,
|
item: this.domainObject,
|
||||||
supportsMultiSelect: true
|
supportsMultiSelect: true
|
||||||
}
|
}
|
||||||
}], multiSelect);
|
}], multiSelect);
|
||||||
event.stopPropagation();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setStatus(status) {
|
setStatus(status) {
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ export default function PlanViewProvider(openmct) {
|
|||||||
name: 'Plan',
|
name: 'Plan',
|
||||||
cssClass: 'icon-plan',
|
cssClass: 'icon-plan',
|
||||||
canView(domainObject) {
|
canView(domainObject) {
|
||||||
return domainObject.type === 'plan';
|
return domainObject.type === 'plan' || domainObject.type === 'gantt-chart';
|
||||||
},
|
},
|
||||||
|
|
||||||
canEdit(domainObject) {
|
canEdit(domainObject) {
|
||||||
return false;
|
return domainObject.type === 'gantt-chart';
|
||||||
},
|
},
|
||||||
|
|
||||||
view: function (domainObject, objectPath) {
|
view: function (domainObject, objectPath) {
|
||||||
@@ -57,7 +57,8 @@ export default function PlanViewProvider(openmct) {
|
|||||||
provide: {
|
provide: {
|
||||||
openmct,
|
openmct,
|
||||||
domainObject,
|
domainObject,
|
||||||
path: objectPath
|
path: objectPath,
|
||||||
|
composition: openmct.composition.get(domainObject)
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ export default function PlanInspectorViewProvider(openmct) {
|
|||||||
template: '<plan-activities-view></plan-activities-view>'
|
template: '<plan-activities-view></plan-activities-view>'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
priority: function () {
|
||||||
|
return openmct.priority.HIGH + 1;
|
||||||
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (component) {
|
if (component) {
|
||||||
component.$destroy();
|
component.$destroy();
|
||||||
@@ -61,9 +64,6 @@ export default function PlanInspectorViewProvider(openmct) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
|
||||||
priority: function () {
|
|
||||||
return openmct.priority.HIGH + 1;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,27 @@
|
|||||||
|
|
||||||
import PlanViewProvider from './PlanViewProvider';
|
import PlanViewProvider from './PlanViewProvider';
|
||||||
import PlanInspectorViewProvider from "./inspector/PlanInspectorViewProvider";
|
import PlanInspectorViewProvider from "./inspector/PlanInspectorViewProvider";
|
||||||
|
import ganttChartCompositionPolicy from './GanttChartCompositionPolicy';
|
||||||
|
|
||||||
export default function (configuration) {
|
export default function (configuration) {
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
openmct.types.addType('plan', {
|
openmct.types.addType('plan', {
|
||||||
name: 'Plan',
|
name: 'Plan',
|
||||||
key: 'plan',
|
key: 'plan',
|
||||||
|
description: 'A non-configurable timeline-like view for a compatible mission plan file.',
|
||||||
|
creatable: false,
|
||||||
|
cssClass: 'icon-plan',
|
||||||
|
form: [],
|
||||||
|
initialize: function (domainObject) {
|
||||||
|
domainObject.configuration = {
|
||||||
|
clipActivityNames: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Name TBD and subject to change
|
||||||
|
openmct.types.addType('gantt-chart', {
|
||||||
|
name: 'Gantt Chart',
|
||||||
|
key: 'gantt-chart',
|
||||||
description: 'A configurable timeline-like view for a compatible mission plan file.',
|
description: 'A configurable timeline-like view for a compatible mission plan file.',
|
||||||
creatable: true,
|
creatable: true,
|
||||||
cssClass: 'icon-plan',
|
cssClass: 'icon-plan',
|
||||||
@@ -36,19 +51,34 @@ export default function (configuration) {
|
|||||||
name: 'Upload Plan (JSON File)',
|
name: 'Upload Plan (JSON File)',
|
||||||
key: 'selectFile',
|
key: 'selectFile',
|
||||||
control: 'file-input',
|
control: 'file-input',
|
||||||
required: true,
|
required: false,
|
||||||
text: 'Select File...',
|
text: 'Select File...',
|
||||||
type: 'application/json',
|
type: 'application/json',
|
||||||
property: [
|
property: [
|
||||||
"selectFile"
|
"selectFile"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Clip Activity Names',
|
||||||
|
key: 'clipActivityNames',
|
||||||
|
control: 'toggleSwitch',
|
||||||
|
cssClass: 'l-input',
|
||||||
|
property: [
|
||||||
|
"configuration",
|
||||||
|
"clipActivityNames"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
initialize: function (domainObject) {
|
initialize(domainObject) {
|
||||||
|
domainObject.configuration = {
|
||||||
|
clipActivityNames: true
|
||||||
|
};
|
||||||
|
domainObject.composition = [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
openmct.objectViews.addProvider(new PlanViewProvider(openmct));
|
openmct.objectViews.addProvider(new PlanViewProvider(openmct));
|
||||||
openmct.inspectorViews.addProvider(new PlanInspectorViewProvider(openmct));
|
openmct.inspectorViews.addProvider(new PlanInspectorViewProvider(openmct));
|
||||||
|
openmct.composition.addPolicy(ganttChartCompositionPolicy(openmct));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import Properties from "../inspectorViews/properties/Properties.vue";
|
|||||||
|
|
||||||
describe('the plugin', function () {
|
describe('the plugin', function () {
|
||||||
let planDefinition;
|
let planDefinition;
|
||||||
|
let ganttDefinition;
|
||||||
let element;
|
let element;
|
||||||
let child;
|
let child;
|
||||||
let openmct;
|
let openmct;
|
||||||
@@ -50,6 +51,7 @@ describe('the plugin', function () {
|
|||||||
openmct.install(new PlanPlugin());
|
openmct.install(new PlanPlugin());
|
||||||
|
|
||||||
planDefinition = openmct.types.get('plan').definition;
|
planDefinition = openmct.types.get('plan').definition;
|
||||||
|
ganttDefinition = openmct.types.get('gantt-chart').definition;
|
||||||
|
|
||||||
element = document.createElement('div');
|
element = document.createElement('div');
|
||||||
element.style.width = '640px';
|
element.style.width = '640px';
|
||||||
@@ -74,15 +76,30 @@ describe('the plugin', function () {
|
|||||||
let mockPlanObject = {
|
let mockPlanObject = {
|
||||||
name: 'Plan',
|
name: 'Plan',
|
||||||
key: 'plan',
|
key: 'plan',
|
||||||
|
creatable: false
|
||||||
|
};
|
||||||
|
|
||||||
|
let mockGanttObject = {
|
||||||
|
name: 'Gantt',
|
||||||
|
key: 'gantt-chart',
|
||||||
creatable: true
|
creatable: true
|
||||||
};
|
};
|
||||||
|
|
||||||
it('defines a plan object type with the correct key', () => {
|
describe('the plan type', () => {
|
||||||
expect(planDefinition.key).toEqual(mockPlanObject.key);
|
it('defines a plan object type with the correct key', () => {
|
||||||
|
expect(planDefinition.key).toEqual(mockPlanObject.key);
|
||||||
|
});
|
||||||
|
it('is not creatable', () => {
|
||||||
|
expect(planDefinition.creatable).toEqual(mockPlanObject.creatable);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
describe('the gantt-chart type', () => {
|
||||||
it('is creatable', () => {
|
it('defines a gantt-chart object type with the correct key', () => {
|
||||||
expect(planDefinition.creatable).toEqual(mockPlanObject.creatable);
|
expect(ganttDefinition.key).toEqual(mockGanttObject.key);
|
||||||
|
});
|
||||||
|
it('is creatable', () => {
|
||||||
|
expect(ganttDefinition.creatable).toEqual(mockGanttObject.creatable);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('the plan view', () => {
|
describe('the plan view', () => {
|
||||||
@@ -107,7 +124,7 @@ describe('the plugin', function () {
|
|||||||
|
|
||||||
const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);
|
const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);
|
||||||
let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
|
let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
|
||||||
expect(planView.canEdit()).toBeFalse();
|
expect(planView.canEdit(testViewObject)).toBeFalse();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -179,10 +196,10 @@ describe('the plugin', function () {
|
|||||||
|
|
||||||
it('displays the group label', () => {
|
it('displays the group label', () => {
|
||||||
const labelEl = element.querySelector('.c-plan__contents .c-object-label .c-object-label__name');
|
const labelEl = element.querySelector('.c-plan__contents .c-object-label .c-object-label__name');
|
||||||
expect(labelEl.innerHTML).toEqual('TEST-GROUP');
|
expect(labelEl.innerHTML).toMatch(/TEST-GROUP/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays the activities and their labels', (done) => {
|
it('displays the activities and their labels', async () => {
|
||||||
const bounds = {
|
const bounds = {
|
||||||
start: 1597160002854,
|
start: 1597160002854,
|
||||||
end: 1597181232854
|
end: 1597181232854
|
||||||
@@ -190,27 +207,22 @@ describe('the plugin', function () {
|
|||||||
|
|
||||||
openmct.time.bounds(bounds);
|
openmct.time.bounds(bounds);
|
||||||
|
|
||||||
Vue.nextTick(() => {
|
await Vue.nextTick();
|
||||||
const rectEls = element.querySelectorAll('.c-plan__contents rect');
|
const rectEls = element.querySelectorAll('.c-plan__contents rect');
|
||||||
expect(rectEls.length).toEqual(2);
|
expect(rectEls.length).toEqual(2);
|
||||||
const textEls = element.querySelectorAll('.c-plan__contents text');
|
const textEls = element.querySelectorAll('.c-plan__contents text');
|
||||||
expect(textEls.length).toEqual(3);
|
expect(textEls.length).toEqual(3);
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it ('shows the status indicator when available', (done) => {
|
it ('shows the status indicator when available', async () => {
|
||||||
openmct.status.set({
|
openmct.status.set({
|
||||||
key: "test-object",
|
key: "test-object",
|
||||||
namespace: ''
|
namespace: ''
|
||||||
}, 'draft');
|
}, 'draft');
|
||||||
|
|
||||||
Vue.nextTick(() => {
|
await Vue.nextTick();
|
||||||
const statusEl = element.querySelector('.c-plan__contents .is-status--draft');
|
const statusEl = element.querySelector('.c-plan__contents .is-status--draft');
|
||||||
expect(statusEl).toBeDefined();
|
expect(statusEl).toBeDefined();
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -224,10 +236,12 @@ describe('the plugin', function () {
|
|||||||
key: 'test-plan',
|
key: 'test-plan',
|
||||||
namespace: ''
|
namespace: ''
|
||||||
},
|
},
|
||||||
|
created: 123456789,
|
||||||
|
modified: 123456790,
|
||||||
version: 'v1'
|
version: 'v1'
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
openmct.selection.select([{
|
openmct.selection.select([{
|
||||||
element: element,
|
element: element,
|
||||||
context: {
|
context: {
|
||||||
@@ -241,19 +255,18 @@ describe('the plugin', function () {
|
|||||||
}
|
}
|
||||||
}], false);
|
}], false);
|
||||||
|
|
||||||
return Vue.nextTick().then(() => {
|
await Vue.nextTick();
|
||||||
let viewContainer = document.createElement('div');
|
let viewContainer = document.createElement('div');
|
||||||
child.append(viewContainer);
|
child.append(viewContainer);
|
||||||
component = new Vue({
|
component = new Vue({
|
||||||
el: viewContainer,
|
el: viewContainer,
|
||||||
components: {
|
components: {
|
||||||
Properties
|
Properties
|
||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
openmct: openmct
|
openmct: openmct
|
||||||
},
|
},
|
||||||
template: '<properties/>'
|
template: '<properties/>'
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -264,7 +277,6 @@ describe('the plugin', function () {
|
|||||||
it('provides an inspector view with the version information if available', () => {
|
it('provides an inspector view with the version information if available', () => {
|
||||||
componentObject = component.$root.$children[0];
|
componentObject = component.$root.$children[0];
|
||||||
const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row');
|
const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row');
|
||||||
expect(propertiesEls.length).toEqual(7);
|
|
||||||
const found = Array.from(propertiesEls).some((propertyEl) => {
|
const found = Array.from(propertiesEls).some((propertyEl) => {
|
||||||
return (propertyEl.children[0].innerHTML.trim() === 'Version'
|
return (propertyEl.children[0].innerHTML.trim() === 'Version'
|
||||||
&& propertyEl.children[1].innerHTML.trim() === 'v1');
|
&& propertyEl.children[1].innerHTML.trim() === 'v1');
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ export default function PlotsInspectorViewProvider(openmct) {
|
|||||||
template: '<plot-options></plot-options>'
|
template: '<plot-options></plot-options>'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
priority: function () {
|
||||||
|
return openmct.priority.HIGH + 1;
|
||||||
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (component) {
|
if (component) {
|
||||||
component.$destroy();
|
component.$destroy();
|
||||||
@@ -51,9 +54,6 @@ export default function PlotsInspectorViewProvider(openmct) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
|
||||||
priority: function () {
|
|
||||||
return openmct.priority.HIGH + 1;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ export default function StackedPlotsInspectorViewProvider(openmct) {
|
|||||||
template: '<plot-options></plot-options>'
|
template: '<plot-options></plot-options>'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
priority: function () {
|
||||||
|
return openmct.priority.HIGH + 1;
|
||||||
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (component) {
|
if (component) {
|
||||||
component.$destroy();
|
component.$destroy();
|
||||||
@@ -49,9 +52,6 @@ export default function StackedPlotsInspectorViewProvider(openmct) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
|
||||||
priority: function () {
|
|
||||||
return openmct.priority.HIGH + 1;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ export default function TimeListInspectorViewProvider(openmct) {
|
|||||||
template: '<timelist-properties-view></timelist-properties-view>'
|
template: '<timelist-properties-view></timelist-properties-view>'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
priority: function () {
|
||||||
|
return openmct.priority.HIGH + 1;
|
||||||
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
if (component) {
|
if (component) {
|
||||||
component.$destroy();
|
component.$destroy();
|
||||||
@@ -62,9 +65,6 @@ export default function TimeListInspectorViewProvider(openmct) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
|
||||||
priority: function () {
|
|
||||||
return openmct.priority.HIGH + 1;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user