Compare commits

...

14 Commits

Author SHA1 Message Date
Jesse Mazzella
911bb0b8e9 style: remove rx, increase min width
- round widths to nearest integer
2023-03-11 22:43:20 +00:00
Jesse Mazzella
ce567c387c refactor: clean up more string literals 2023-03-11 22:43:20 +00:00
Jesse Mazzella
55d125d429 chore: run lint:fix
- eslint sez no danglin' commas! EVER!
2023-03-11 22:43:20 +00:00
Jesse Mazzella
ab3c52c9b2 docs: add TODO 2023-03-11 22:43:20 +00:00
Jesse Mazzella
f354e1d263 tes(e2e): fix plan test 2023-03-11 22:43:20 +00:00
Jesse Mazzella
7ff366c342 test: fix tests, add basic tests for gantt 2023-03-11 22:43:20 +00:00
Jesse Mazzella
fe38f17069 feat: replace Plan if another is dragged in
- SImilar to Gauges or Scatter Plots, launch a confirmation dialog to replace the existing Plan with another, if another Plan is dragged into the chart.
2023-03-11 22:43:20 +00:00
Jesse Mazzella
38c5054e30 fix: inspector view tab priority
- fixes issue where inspector view priorities were not being passed to the view registry
2023-03-11 22:43:20 +00:00
Jesse Mazzella
18bacfd251 feat: compose a Gantt Chart using a Plan
- Allows Plans to be dragged into Gantt Charts (name tentative) to create a configurable Activity View

- Clip/Unclip activity names by editing domainObject property
2023-03-11 22:43:20 +00:00
Jesse Mazzella
7ecfebb352 feat(WIP): name clipping via clipPath elements 2023-03-11 22:43:20 +00:00
Jesse Mazzella
c7fb6079ef refactor: general code cleanup 2023-03-11 22:43:20 +00:00
Jesse Mazzella
6193f79ce0 feat: add clipActivityNames property
- refactor: initialize data to `null`
2023-03-11 22:43:20 +00:00
Jesse Mazzella
3422853d3d feat: add initial configurable plan type
- Name change TBD
2023-03-11 22:43:20 +00:00
Jesse Mazzella
bc550d366b refactor: convert Type API to ES6 module
- Another AMD module bites the dust 🧹
2023-03-11 22:43:20 +00:00
20 changed files with 486 additions and 295 deletions

View File

@@ -168,8 +168,8 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
// Click the Create button
await page.click('button:has-text("Create")');
// Click 'Plan' menu option
await page.click(`li:text("Plan")`);
// Click 'Gantt Chart' menu option
await page.click(`li:text("Gantt Chart")`);
// Modify the name input field of the domain object to accept 'name'
if (name) {

View File

@@ -68,7 +68,7 @@ const testPlan = {
};
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' });
const plan = await createPlanFromJSON(page, {

View File

@@ -71,7 +71,7 @@ function (
StatusAPI: StatusAPI.default,
TelemetryAPI: TelemetryAPI,
TimeAPI: TimeAPI.default,
TypeRegistry: TypeRegistry,
TypeRegistry: TypeRegistry.default,
UserAPI: UserAPI.default,
AnnotationAPI: AnnotationAPI.default
};

View File

@@ -20,8 +20,6 @@
* 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.
@@ -30,53 +28,17 @@ define(function () {
* @class Type
* @memberof module:openmct
*/
function Type(definition) {
export default class Type {
constructor(definition) {
this.definition = definition;
if (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.
*/
Type.definitionFromLegacyDefinition = function (legacyDefinition) {
static definitionFromLegacyDefinition(legacyDefinition) {
let definition = {};
definition.name = legacyDefinition.name;
definition.cssClass = legacyDefinition.cssClass;
@@ -121,7 +83,39 @@ define(function () {
}
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;
}
}

View File

@@ -19,7 +19,8 @@
* this source code distribution or the Licensing information page available
* 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",
@@ -44,10 +45,10 @@ define(['./Type'], function (Type) {
* @interface TypeRegistry
* @memberof module:openmct
*/
function TypeRegistry() {
export default class TypeRegistry {
constructor() {
this.types = {};
}
/**
* Register a new object type.
*
@@ -56,17 +57,16 @@ define(['./Type'], function (Type) {
* @method addType
* @memberof module:openmct.TypeRegistry#
*/
TypeRegistry.prototype.addType = function (typeKey, typeDef) {
addType(typeKey, typeDef) {
this.standardizeType(typeDef);
this.types[typeKey] = new Type(typeDef);
};
}
/**
* Takes a typeDef, standardizes it, and logs warnings about unsupported
* usage.
* @private
*/
TypeRegistry.prototype.standardizeType = function (typeDef) {
standardizeType(typeDef) {
if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) {
if (!typeDef.name) {
typeDef.name = typeDef.label;
@@ -74,18 +74,16 @@ define(['./Type'], function (Type) {
delete typeDef.label;
}
};
}
/**
* List keys for all registered types.
* @method listKeys
* @memberof module:openmct.TypeRegistry#
* @returns {string[]} all registered type keys
*/
TypeRegistry.prototype.listKeys = function () {
listKeys() {
return Object.keys(this.types);
};
}
/**
* Retrieve a registered type by its key.
* @method get
@@ -93,18 +91,15 @@ define(['./Type'], function (Type) {
* @memberof module:openmct.TypeRegistry#
* @returns {module:openmct.Type} the registered type
*/
TypeRegistry.prototype.get = function (typeKey) {
get(typeKey) {
return this.types[typeKey] || UNKNOWN_TYPE;
};
TypeRegistry.prototype.importLegacyTypes = function (types) {
}
importLegacyTypes(types) {
types.filter((t) => this.get(t.key) === UNKNOWN_TYPE)
.forEach((type) => {
let def = Type.definitionFromLegacyDefinition(type);
this.addType(type.key, def);
});
};
return TypeRegistry;
});
}
}

View File

@@ -20,7 +20,8 @@
* 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;
@@ -52,4 +53,3 @@ define(['./TypeRegistry', './Type'], function (TypeRegistry, Type) {
expect(typeRegistryInstance.listKeys ()).toContain('testType');
});
});
});

View File

@@ -33,6 +33,9 @@ export default function BarGraphInspectorViewProvider(openmct) {
template: '<bar-graph-options></bar-graph-options>'
});
},
priority: function () {
return openmct.priority.HIGH + 1;
},
destroy: function () {
if (component) {
component.$destroy();
@@ -40,9 +43,6 @@ export default function BarGraphInspectorViewProvider(openmct) {
}
}
};
},
priority: function () {
return openmct.priority.HIGH + 1;
}
};
}

View File

@@ -33,6 +33,9 @@ export default function ScatterPlotInspectorViewProvider(openmct) {
template: '<plot-options></plot-options>'
});
},
priority: function () {
return openmct.priority.HIGH + 1;
},
destroy: function () {
if (component) {
component.$destroy();
@@ -40,9 +43,6 @@ export default function ScatterPlotInspectorViewProvider(openmct) {
}
}
};
},
priority: function () {
return openmct.priority.HIGH + 1;
}
};
}

View File

@@ -56,6 +56,9 @@ export default function FaultManagementInspectorViewProvider(openmct) {
template: '<FaultManagementInspector></FaultManagementInspector>'
});
},
priority: function () {
return openmct.priority.HIGH + 1;
},
destroy: function () {
if (component) {
component.$destroy();
@@ -63,9 +66,6 @@ export default function FaultManagementInspectorViewProvider(openmct) {
}
}
};
},
priority: function () {
return openmct.priority.HIGH + 1;
}
};
}

View File

@@ -61,6 +61,9 @@ define([
return hasPersistedFilters || hasGlobalFilters;
},
priority: function () {
return openmct.priority.DEFAULT;
},
destroy: function () {
if (component) {
component.$destroy();
@@ -68,9 +71,6 @@ define([
}
}
};
},
priority: function () {
return openmct.priority.DEFAULT;
}
};
}

View File

@@ -76,14 +76,14 @@ export default function StylesInspectorViewProvider(openmct) {
template: `<StylesInspectorView />`
});
},
priority: function () {
return openmct.priority.DEFAULT;
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return this.openmct.priority.DEFAULT;
}
};
}

View 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;
};
}

View File

@@ -51,6 +51,7 @@ import TimelineAxis from "../../ui/components/TimeSystemAxis.vue";
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
import { getValidatedData } from "./util";
import Vue from "vue";
import { v4 as uuid } from "uuid";
const PADDING = 1;
const OUTER_TEXT_PADDING = 12;
@@ -61,15 +62,16 @@ const RESIZE_POLL_INTERVAL = 200;
const ROW_HEIGHT = 25;
const LINE_HEIGHT = 12;
const MAX_TEXT_WIDTH = 300;
const EDGE_ROUNDING = 5;
const MIN_ACTIVITY_WIDTH = 2;
const DEFAULT_COLOR = '#cc9922';
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
export default {
components: {
TimelineAxis,
SwimLane
},
inject: ['openmct', 'domainObject', 'path'],
inject: ['openmct', 'domainObject', 'path', 'composition'],
props: {
options: {
type: Object,
@@ -88,8 +90,9 @@ export default {
},
data() {
return {
viewBounds: undefined,
timeSystem: undefined,
viewBounds: null,
timeSystem: null,
clipActivityNames: this.domainObject?.configuration?.clipActivityNames ?? false,
height: 0
};
},
@@ -103,9 +106,10 @@ export default {
this.setDimensions();
this.setTimeContext();
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.status = this.openmct.status.get(this.domainObject.identifier);
this.stopObservingConfig = this.openmct.objects.observe(this.domainObject, 'configuration', this.handleConfigurationChange);
this.loadComposition();
},
beforeDestroy() {
clearInterval(this.resizeTimer);
@@ -117,8 +121,18 @@ export default {
if (this.removeStatusListener) {
this.removeStatusListener();
}
if (this.composition) {
this.composition.off('add', this.handleCompositionAdd);
this.composition.off('remove', this.handleCompositionRemove);
}
this.stopObservingConfig();
},
methods: {
activityNameFitsRect(activityName, rectWidth) {
return (this.getTextWidth(activityName) + TEXT_LEFT_PADDING) < rectWidth;
},
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
@@ -131,6 +145,14 @@ export default {
this.timeContext.on("bounds", this.updateViewBounds);
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() {
if (this.timeContext) {
this.timeContext.off("timeSystem", this.setScaleAndPlotActivities);
@@ -138,6 +160,63 @@ export default {
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) {
this.getPlanData(mutatedObject);
this.setScaleAndPlotActivities();
@@ -193,14 +272,14 @@ export default {
this.viewBounds = Object.create(bounds);
}
if (this.timeSystem === undefined) {
if (this.timeSystem === null) {
this.timeSystem = this.openmct.time.timeSystem();
}
this.setScaleAndPlotActivities();
},
setScaleAndPlotActivities(timeSystem) {
if (timeSystem !== undefined) {
if (timeSystem) {
this.timeSystem = timeSystem;
}
@@ -224,7 +303,7 @@ export default {
return;
}
if (timeSystem === undefined) {
if (!timeSystem) {
timeSystem = this.openmct.time.timeSystem();
}
@@ -245,10 +324,15 @@ export default {
isActivityInBounds(activity) {
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) {
const numA = parseInt(a, 10);
@@ -303,35 +387,37 @@ export default {
let activities = this.planData[key];
activities.forEach((activity) => {
if (this.isActivityInBounds(activity)) {
if (!this.isActivityInBounds(activity)) {
return;
}
const currentStart = Math.max(this.viewBounds.start, activity.start);
const currentEnd = Math.min(this.viewBounds.end, activity.end);
const rectX = this.xScale(currentStart);
const rectY = this.xScale(currentEnd);
const rectWidth = rectY - rectX;
const rectX1 = this.xScale(currentStart);
const rectX2 = this.xScale(currentEnd);
const rectWidth = rectX2 - rectX1;
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 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 (activityNameFitsRect) {
} else if (showTextInsideRect) {
textColor = this.getContrastingColor(color);
}
let textLines = this.getActivityDisplayText(this.canvasContext, activity.name, activityNameFitsRect);
const textLines = this.getActivityDisplayText(this.canvasContext, activity.name, showTextInsideRect);
const textWidth = textStart + this.getTextWidth(textLines[0]) + TEXT_LEFT_PADDING;
if (activityNameFitsRect) {
currentRow = this.getRowForActivity(rectX, rectWidth, activitiesByRow);
if (showTextInsideRect) {
currentRow = this.getRowForActivity(rectX1, rectWidth, activitiesByRow);
} else {
currentRow = this.getRowForActivity(rectX, textWidth, activitiesByRow);
currentRow = this.getRowForActivity(rectX1, textWidth, activitiesByRow);
}
let textY = parseInt(currentRow, 10) + (activityNameFitsRect ? INNER_TEXT_PADDING : OUTER_TEXT_PADDING);
let textY = parseInt(currentRow, 10) + (showTextInsideRect ? INNER_TEXT_PADDING : OUTER_TEXT_PADDING);
if (!activitiesByRow[currentRow]) {
activitiesByRow[currentRow] = [];
@@ -351,13 +437,12 @@ export default {
},
textLines: textLines,
textStart: textStart,
textClass: activityNameFitsRect ? "" : "activity-label--outside-rect",
textClass: showTextInsideRect ? "" : "activity-label--outside-rect",
textY: textY,
start: rectX,
end: activityNameFitsRect ? rectY : textStart + textWidth,
start: rectX1,
end: showTextInsideRect ? rectX2 : textStart + textWidth,
rectWidth: rectWidth
});
}
});
this.groupActivities[key] = {
heading: key,
@@ -365,28 +450,32 @@ export default {
};
});
},
getActivityDisplayText(context, text, activityNameFitsRect) {
/**
* Format the activity name to fit within the activity rect with a max of 2 lines
* @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 = text.split(' ');
let words = activityName.split(' ');
let line = '';
let activityText = [];
let rows = 1;
let activityLines = [];
for (let n = 0; (n < words.length) && (rows <= 2); n++) {
let testLine = line + words[n] + ' ';
let metrics = context.measureText(testLine);
let testWidth = metrics.width;
if (!activityNameFitsRect && (testWidth > MAX_TEXT_WIDTH && n > 0)) {
activityText.push(line);
for (let n = 0; (n < words.length) && (activityLines.length <= 2); n++) {
let tempLine = line + words[n] + ' ';
let textMetrics = canvasContext.measureText(tempLine);
const textWidth = textMetrics.width;
if (!activityNameFitsRect && (textWidth > MAX_TEXT_WIDTH && n > 0)) {
activityLines.push(line);
line = words[n] + ' ';
testLine = line + words[n] + ' ';
rows = rows + 1;
tempLine = line + words[n] + ' ';
}
line = testLine;
line = tempLine;
}
return activityText.length ? activityText : [line];
return activityLines.length ? activityLines : [line];
},
getGroupContainer(activityRows, heading) {
let svgHeight = 30;
@@ -418,7 +507,24 @@ export default {
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);
@@ -432,7 +538,6 @@ export default {
};
},
drawPlan() {
Object.keys(this.groupActivities).forEach((group, index) => {
const activitiesByRow = this.groupActivities[group].activitiesByRow;
const heading = this.groupActivities[group].heading;
@@ -454,7 +559,7 @@ export default {
});
},
plotNoItems(svgElement) {
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
const textElement = document.createElementNS(SVG_NAMESPACE, 'text');
this.setNSAttributesForElement(textElement, {
x: "10",
y: "20",
@@ -464,6 +569,10 @@ export default {
svgElement.appendChild(textElement);
},
/**
* @param {Element} element
* @param {Object} attributes
*/
setNSAttributesForElement(element, attributes) {
Object.keys(attributes).forEach((key) => {
element.setAttributeNS(null, key, attributes[key]);
@@ -474,7 +583,7 @@ export default {
},
// Experimental for now - unused
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, {
width: String(MAX_TEXT_WIDTH),
height: String(LINE_HEIGHT * 2),
@@ -490,27 +599,33 @@ export default {
svgElement.appendChild(foreign);
},
// TODO: Clean up, extract HTML element creation into utility functions
plotActivity(item, row, svgElement) {
const activity = item.activity;
let width = item.rectWidth;
let rectElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
const rectElement = document.createElementNS(SVG_NAMESPACE, 'rect');
const width = Math.max(Math.round(item.rectWidth), MIN_ACTIVITY_WIDTH);
const clipUuid = uuid();
if (item.activity.exceeds.start) {
width = width + EDGE_ROUNDING;
if (this.clipActivityNames) {
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, {
class: 'activity-bounds',
x: item.activity.exceeds.start ? item.start - EDGE_ROUNDING : item.start,
x: Math.round(item.start),
y: row,
rx: (width < EDGE_ROUNDING * 2) ? 0 : EDGE_ROUNDING,
width: width,
height: String(ROW_HEIGHT),
fill: activity.color
@@ -518,12 +633,13 @@ export default {
rectElement.addEventListener('click', (event) => {
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
event.stopPropagation();
});
svgElement.appendChild(rectElement);
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, {
class: `activity-label ${item.textClass}`,
x: item.textStart,
@@ -531,10 +647,17 @@ export default {
fill: activity.textColor
});
if (this.clipActivityNames) {
this.setNSAttributesForElement(textElement, {
'clip-path': `url(#clip-${clipUuid})`
});
}
const textNode = document.createTextNode(line);
textElement.appendChild(textNode);
textElement.addEventListener('click', (event) => {
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
event.stopPropagation();
});
svgElement.appendChild(textElement);
});
@@ -566,6 +689,7 @@ export default {
setSelectionForActivity(element, activity, multiSelect) {
this.openmct.selection.select([{
element: element,
// activity: activity,
context: {
type: 'activity',
activity: activity
@@ -573,11 +697,11 @@ export default {
}, {
element: this.openmct.layout.$refs.browseObject.$el,
context: {
// activity: activity,
item: this.domainObject,
supportsMultiSelect: true
}
}], multiSelect);
event.stopPropagation();
},
setStatus(status) {

View File

@@ -35,11 +35,11 @@ export default function PlanViewProvider(openmct) {
name: 'Plan',
cssClass: 'icon-plan',
canView(domainObject) {
return domainObject.type === 'plan';
return domainObject.type === 'plan' || domainObject.type === 'gantt-chart';
},
canEdit(domainObject) {
return false;
return domainObject.type === 'gantt-chart';
},
view: function (domainObject, objectPath) {
@@ -57,7 +57,8 @@ export default function PlanViewProvider(openmct) {
provide: {
openmct,
domainObject,
path: objectPath
path: objectPath,
composition: openmct.composition.get(domainObject)
},
data() {
return {

View File

@@ -54,6 +54,9 @@ export default function PlanInspectorViewProvider(openmct) {
template: '<plan-activities-view></plan-activities-view>'
});
},
priority: function () {
return openmct.priority.HIGH + 1;
},
destroy: function () {
if (component) {
component.$destroy();
@@ -61,9 +64,6 @@ export default function PlanInspectorViewProvider(openmct) {
}
}
};
},
priority: function () {
return openmct.priority.HIGH + 1;
}
};
}

View File

@@ -22,12 +22,27 @@
import PlanViewProvider from './PlanViewProvider';
import PlanInspectorViewProvider from "./inspector/PlanInspectorViewProvider";
import ganttChartCompositionPolicy from './GanttChartCompositionPolicy';
export default function (configuration) {
return function install(openmct) {
openmct.types.addType('plan', {
name: '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.',
creatable: true,
cssClass: 'icon-plan',
@@ -36,19 +51,34 @@ export default function (configuration) {
name: 'Upload Plan (JSON File)',
key: 'selectFile',
control: 'file-input',
required: true,
required: false,
text: 'Select File...',
type: 'application/json',
property: [
"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.inspectorViews.addProvider(new PlanInspectorViewProvider(openmct));
openmct.composition.addPolicy(ganttChartCompositionPolicy(openmct));
};
}

View File

@@ -27,6 +27,7 @@ import Properties from "../inspectorViews/properties/Properties.vue";
describe('the plugin', function () {
let planDefinition;
let ganttDefinition;
let element;
let child;
let openmct;
@@ -50,6 +51,7 @@ describe('the plugin', function () {
openmct.install(new PlanPlugin());
planDefinition = openmct.types.get('plan').definition;
ganttDefinition = openmct.types.get('gantt-chart').definition;
element = document.createElement('div');
element.style.width = '640px';
@@ -74,16 +76,31 @@ describe('the plugin', function () {
let mockPlanObject = {
name: 'Plan',
key: 'plan',
creatable: false
};
let mockGanttObject = {
name: 'Gantt',
key: 'gantt-chart',
creatable: true
};
describe('the plan type', () => {
it('defines a plan object type with the correct key', () => {
expect(planDefinition.key).toEqual(mockPlanObject.key);
});
it('is creatable', () => {
it('is not creatable', () => {
expect(planDefinition.creatable).toEqual(mockPlanObject.creatable);
});
});
describe('the gantt-chart type', () => {
it('defines a gantt-chart object type with the correct key', () => {
expect(ganttDefinition.key).toEqual(mockGanttObject.key);
});
it('is creatable', () => {
expect(ganttDefinition.creatable).toEqual(mockGanttObject.creatable);
});
});
describe('the plan view', () => {
it('provides a plan view', () => {
@@ -107,7 +124,7 @@ describe('the plugin', function () {
const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);
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', () => {
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 = {
start: 1597160002854,
end: 1597181232854
@@ -190,27 +207,22 @@ describe('the plugin', function () {
openmct.time.bounds(bounds);
Vue.nextTick(() => {
await Vue.nextTick();
const rectEls = element.querySelectorAll('.c-plan__contents rect');
expect(rectEls.length).toEqual(2);
const textEls = element.querySelectorAll('.c-plan__contents text');
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({
key: "test-object",
namespace: ''
}, 'draft');
Vue.nextTick(() => {
await Vue.nextTick();
const statusEl = element.querySelector('.c-plan__contents .is-status--draft');
expect(statusEl).toBeDefined();
done();
});
});
});
@@ -224,10 +236,12 @@ describe('the plugin', function () {
key: 'test-plan',
namespace: ''
},
created: 123456789,
modified: 123456790,
version: 'v1'
};
beforeEach(() => {
beforeEach(async () => {
openmct.selection.select([{
element: element,
context: {
@@ -241,7 +255,7 @@ describe('the plugin', function () {
}
}], false);
return Vue.nextTick().then(() => {
await Vue.nextTick();
let viewContainer = document.createElement('div');
child.append(viewContainer);
component = new Vue({
@@ -255,7 +269,6 @@ describe('the plugin', function () {
template: '<properties/>'
});
});
});
afterEach(() => {
component.$destroy();
@@ -264,7 +277,6 @@ describe('the plugin', function () {
it('provides an inspector view with the version information if available', () => {
componentObject = component.$root.$children[0];
const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row');
expect(propertiesEls.length).toEqual(7);
const found = Array.from(propertiesEls).some((propertyEl) => {
return (propertyEl.children[0].innerHTML.trim() === 'Version'
&& propertyEl.children[1].innerHTML.trim() === 'v1');

View File

@@ -44,6 +44,9 @@ export default function PlotsInspectorViewProvider(openmct) {
template: '<plot-options></plot-options>'
});
},
priority: function () {
return openmct.priority.HIGH + 1;
},
destroy: function () {
if (component) {
component.$destroy();
@@ -51,9 +54,6 @@ export default function PlotsInspectorViewProvider(openmct) {
}
}
};
},
priority: function () {
return openmct.priority.HIGH + 1;
}
};
}

View File

@@ -42,6 +42,9 @@ export default function StackedPlotsInspectorViewProvider(openmct) {
template: '<plot-options></plot-options>'
});
},
priority: function () {
return openmct.priority.HIGH + 1;
},
destroy: function () {
if (component) {
component.$destroy();
@@ -49,9 +52,6 @@ export default function StackedPlotsInspectorViewProvider(openmct) {
}
}
};
},
priority: function () {
return openmct.priority.HIGH + 1;
}
};
}

View File

@@ -55,6 +55,9 @@ export default function TimeListInspectorViewProvider(openmct) {
template: '<timelist-properties-view></timelist-properties-view>'
});
},
priority: function () {
return openmct.priority.HIGH + 1;
},
destroy: function () {
if (component) {
component.$destroy();
@@ -62,9 +65,6 @@ export default function TimeListInspectorViewProvider(openmct) {
}
}
};
},
priority: function () {
return openmct.priority.HIGH + 1;
}
};
}