Compare commits

...

11 Commits

19 changed files with 952 additions and 63 deletions

View File

@@ -108,6 +108,7 @@
openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.ActivityStates());
openmct.install(
openmct.plugins.PlanLayout({
creatable: true

View File

@@ -99,7 +99,13 @@ export default class ObjectAPI {
this.cache = {};
this.interceptorRegistry = new InterceptorRegistry();
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'restricted-notebook', 'plan', 'annotation'];
this.SYNCHRONIZED_OBJECT_TYPES = [
'notebook',
'restricted-notebook',
'plan',
'annotation',
'activity-states'
];
this.errors = {
Conflict: ConflictError

View File

@@ -0,0 +1,51 @@
/*****************************************************************************
* 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.
*****************************************************************************/
import { ACTIVITYSTATES_KEY, ACTIVITYSTATES_TYPE } from './createActivityStatesIdentifier.js';
function activityStatesInterceptor(openmct, identifierObject, name) {
const activityStatesModel = {
identifier: identifierObject,
name,
type: ACTIVITYSTATES_TYPE,
activities: {},
location: null
};
return {
appliesTo: (identifier) => {
return identifier.key === ACTIVITYSTATES_KEY;
},
invoke: (identifier, object) => {
if (!object || openmct.objects.isMissing(object)) {
openmct.objects.save(activityStatesModel);
return activityStatesModel;
}
return object;
},
priority: openmct.priority.HIGH
};
}
export default activityStatesInterceptor;

View File

@@ -0,0 +1,9 @@
export const ACTIVITYSTATES_KEY = 'activity-states';
export const ACTIVITYSTATES_TYPE = 'activity-states';
export function createActivityStatesIdentifier(namespace = '') {
return {
key: ACTIVITYSTATES_KEY,
namespace
};
}

View File

@@ -0,0 +1,42 @@
/*****************************************************************************
* 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.
*****************************************************************************/
import activityStatesInterceptor from './activityStatesInterceptor.js';
import { createActivityStatesIdentifier } from './createActivityStatesIdentifier.js';
const ACTIVITYSTATES_DEFAULT_NAME = 'Activity States';
export default function ActivityStatesPlugin(
name = ACTIVITYSTATES_DEFAULT_NAME,
namespace = '',
priority = undefined
) {
return function install(openmct) {
const identifier = createActivityStatesIdentifier(namespace);
if (priority === undefined) {
priority = openmct.priority.LOW;
}
openmct.objects.addGetInterceptor(activityStatesInterceptor(openmct, identifier, name));
};
}

View File

@@ -0,0 +1,89 @@
/*****************************************************************************
* 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.
*****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing';
import {
ACTIVITYSTATES_KEY,
createActivityStatesIdentifier
} from './createActivityStatesIdentifier.js';
const MISSING_NAME = `Missing: ${ACTIVITYSTATES_KEY}`;
const DEFAULT_NAME = 'Activity States';
const activityStatesIdentifier = createActivityStatesIdentifier();
describe('the plugin', () => {
let openmct;
let missingObj = {
identifier: activityStatesIdentifier,
type: 'unknown',
name: MISSING_NAME
};
describe('with no arguments passed in', () => {
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(openmct.plugins.ActivityStates());
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('when installed, adds "Activity States"', async () => {
const activityStatesObject = await openmct.objects.get(activityStatesIdentifier);
expect(activityStatesObject.name).toBe(DEFAULT_NAME);
expect(activityStatesObject).toBeDefined();
});
describe('adds an interceptor that returns a "Activity States" model for', () => {
let activityStatesObject;
let mockNotFoundProvider;
let activeProvider;
beforeEach(async () => {
mockNotFoundProvider = {
get: () => Promise.reject(new Error('Not found')),
create: () => Promise.resolve(missingObj),
update: () => Promise.resolve(missingObj)
};
activeProvider = mockNotFoundProvider;
spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);
activityStatesObject = await openmct.objects.get(activityStatesIdentifier);
});
it('missing objects', () => {
let idsMatch = openmct.objects.areIdsEqual(
activityStatesObject.identifier,
activityStatesIdentifier
);
expect(activityStatesObject).toBeDefined();
expect(idsMatch).toBeTrue();
});
});
});
});

View File

@@ -488,6 +488,7 @@ export default {
},
start: rawActivity.start,
end: rawActivity.end,
description: rawActivity.description,
row: currentRow,
textLines: textLines,
textStart: textStart,
@@ -496,7 +497,8 @@ export default {
rectStart: rectX1,
rectEnd: showTextInsideRect ? rectX2 : textStart + textWidth,
rectWidth: rectWidth,
clipPathId: this.getClipPathId(groupName, rawActivity, currentRow)
clipPathId: this.getClipPathId(groupName, rawActivity, currentRow),
id: rawActivity.id
};
activitiesByRow[currentRow].push(activity);
});

View File

@@ -20,21 +20,39 @@
at runtime from the About dialog for additional information.
-->
<template>
<div class="c-inspector__properties c-inspect-properties">
<plan-activity-view
<div>
<plan-activity-time-view
v-for="activity in activities"
:key="activity.id"
:key="activity.key"
class="c-inspector__properties c-inspect-properties"
:activity="activity"
:heading="heading"
/>
<plan-activity-properties-view
v-for="activity in activities"
:key="activity.key"
:heading="'Properties'"
class="c-inspector__properties c-inspect-properties"
:activity="activity"
></plan-activity-properties-view>
<plan-activity-status-view
v-if="canPersistState"
:key="activities[0].key"
class="c-inspector__properties c-inspect-properties"
:activity="activities[0]"
:execution-state="persistedActivityStates[activities[0].id]"
:heading="'Activity Status'"
@update-activity-state="persistedActivityState"
/>
</div>
</template>
<script>
import { getPreciseDuration } from 'utils/duration';
import { v4 as uuid } from 'uuid';
import PlanActivityView from './PlanActivityView.vue';
import PlanActivityPropertiesView from './PlanActivityPropertiesView.vue';
import PlanActivityStatusView from './PlanActivityStatusView.vue';
import PlanActivityTimeView from './PlanActivityTimeView.vue';
const propertyLabels = {
start: 'Start DateTime',
@@ -44,23 +62,34 @@ const propertyLabels = {
latestEnd: 'Latest End',
gap: 'Gap',
overlap: 'Overlap',
totalTime: 'Total Time'
totalTime: 'Total Time',
description: 'Description'
};
export default {
components: {
PlanActivityView
PlanActivityTimeView,
PlanActivityPropertiesView,
PlanActivityStatusView
},
inject: ['openmct', 'selection'],
data() {
return {
name: '',
activities: [],
selectedActivities: [],
persistedActivityStates: {},
heading: ''
};
},
computed: {
canPersistState() {
return this.selectedActivities.length === 1 && this.activities[0].id;
}
},
mounted() {
this.setFormatters();
this.getPlanData(this.selection);
this.getActivityStates();
this.getActivities();
this.openmct.selection.on('change', this.updateSelection);
this.openmct.time.on('timeSystem', this.setFormatters);
@@ -68,8 +97,23 @@ export default {
beforeUnmount() {
this.openmct.selection.off('change', this.updateSelection);
this.openmct.time.off('timeSystem', this.setFormatters);
if (this.stopObservingActivityStatesObject) {
this.stopObservingActivityStatesObject();
}
},
methods: {
async getActivityStates() {
this.activityStatesObject = await this.openmct.objects.get('activity-states');
this.setActivityStates();
this.stopObservingActivityStatesObject = this.openmct.objects.observe(
this.activityStatesObject,
'*',
this.setActivityStates
);
},
setActivityStates() {
this.persistedActivityStates = { ...this.activityStatesObject.activities };
},
setFormatters() {
let timeSystem = this.openmct.time.timeSystem();
this.timeFormatter = this.openmct.telemetry.getValueFormatter({
@@ -86,6 +130,7 @@ export default {
if (selectionItem[0].context.type === 'activity') {
const activity = selectionItem[0].context.activity;
if (activity) {
activity.key = activity.id ?? activity.name;
this.selectedActivities.push(activity);
}
}
@@ -104,20 +149,31 @@ export default {
this.activities.splice(0);
this.selectedActivities.forEach((selectedActivity, index) => {
const activity = {
id: uuid(),
start: {
label: propertyLabels.start,
value: this.formatTime(selectedActivity.start)
},
end: {
label: propertyLabels.end,
value: this.formatTime(selectedActivity.end)
},
duration: {
label: propertyLabels.duration,
value: this.formatDuration(selectedActivity.end - selectedActivity.start)
id: selectedActivity.id,
key: selectedActivity.key,
timeProperties: {
start: {
label: propertyLabels.start,
value: this.formatTime(selectedActivity.start)
},
end: {
label: propertyLabels.end,
value: this.formatTime(selectedActivity.end)
},
duration: {
label: propertyLabels.duration,
value: this.formatDuration(selectedActivity.end - selectedActivity.start)
}
}
};
if (selectedActivity.description) {
activity.metadata = {
description: {
label: propertyLabels.description,
value: selectedActivity.description
}
};
}
this.activities[index] = activity;
});
},
@@ -141,6 +197,8 @@ export default {
let latestEnd;
let gap;
let overlap;
let id;
let key;
//Sort by start time
let selectedActivities = this.selectedActivities.sort(this.sortFn);
@@ -159,6 +217,8 @@ export default {
earliestStart = Math.min(earliestStart, selectedActivity.start);
latestEnd = Math.max(latestEnd, selectedActivity.end);
} else {
id = selectedActivity.id;
key = selectedActivity.id ?? selectedActivity.name;
earliestStart = selectedActivity.start;
latestEnd = selectedActivity.end;
}
@@ -166,30 +226,33 @@ export default {
let totalTime = latestEnd - earliestStart;
const activity = {
id: uuid(),
earliestStart: {
label: propertyLabels.earliestStart,
value: this.formatTime(earliestStart)
},
latestEnd: {
label: propertyLabels.latestEnd,
value: this.formatTime(latestEnd)
id,
key,
timeProperties: {
earliestStart: {
label: propertyLabels.earliestStart,
value: this.formatTime(earliestStart)
},
latestEnd: {
label: propertyLabels.latestEnd,
value: this.formatTime(latestEnd)
}
}
};
if (gap) {
activity.gap = {
activity.timeProperties.gap = {
label: propertyLabels.gap,
value: this.formatDuration(gap)
};
} else if (overlap) {
activity.overlap = {
activity.timeProperties.overlap = {
label: propertyLabels.overlap,
value: this.formatDuration(overlap)
};
}
activity.totalTime = {
activity.timeProperties.totalTime = {
label: propertyLabels.totalTime,
value: this.formatDuration(totalTime)
};
@@ -201,6 +264,11 @@ export default {
},
formatTime(time) {
return this.timeFormatter.format(time);
},
persistedActivityState(data) {
const { key, executionState } = data;
const activitiesPath = `activities.${key}`;
this.openmct.objects.mutate(this.activityStatesObject, activitiesPath, executionState);
}
}
};

View File

@@ -0,0 +1,82 @@
<!--
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.
-->
<template>
<div>
<div v-if="properties.length" class="u-contents">
<div class="c-inspect-properties__header">{{ heading }}</div>
<ul v-for="property in properties" :key="property.id" class="c-inspect-properties__section">
<activity-property :label="property.label" :value="property.value" />
</ul>
</div>
</div>
</template>
<script>
import ActivityProperty from './ActivityProperty.vue';
export default {
components: {
ActivityProperty
},
props: {
activity: {
type: Object,
required: true
},
heading: {
type: String,
required: true
}
},
data() {
return {
properties: []
};
},
mounted() {
this.setProperties();
},
methods: {
setProperties() {
if (!this.activity.metadata) {
return;
}
Object.keys(this.activity.metadata).forEach((key) => {
if (this.activity.metadata[key].label) {
const label = this.activity.metadata[key].label;
const value = String(this.activity.metadata[key].value);
const id = this.activity.id;
this.properties[this.properties.length] = {
id,
label,
value
};
}
});
console.log(this.activity.metadata, this.properties);
}
}
};
</script>

View File

@@ -0,0 +1,114 @@
<!--
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.
-->
<template>
<div>
<div class="u-contents">
<div class="c-inspect-properties__header">{{ heading }}</div>
<form name="activityStatus">
<select v-model="currentStatusKey" name="setActivityStatus" @change="changeActivityStatus">
<option v-for="status in activityStates" :key="status.key" :value="status.label">
{{ status.label }}
</option>
</select>
</form>
</div>
</div>
</template>
<script>
const activityStates = [
{
key: '',
label: '- Select Activity Status -'
},
{
key: 'active',
label: 'In progress'
},
{
key: 'completed',
label: 'Completed'
},
{
key: 'aborted',
label: 'Aborted'
},
{
key: 'cancelled',
label: 'Cancelled'
},
{
key: 'notStarted',
label: 'Not started'
}
];
export default {
props: {
activity: {
type: Object,
required: true
},
executionState: {
type: String,
default() {
return '';
}
},
heading: {
type: String,
required: true
}
},
emits: ['updateActivityState'],
data() {
return {
activityStates: activityStates,
currentStatusKey: activityStates[0].key
};
},
watch: {
executionState() {
this.setActivityStatus();
}
},
mounted() {
this.setActivityStatus();
},
methods: {
setActivityStatus() {
this.currentStatusKey = this.executionState;
},
changeActivityStatus() {
if (this.currentStatusKey === '') {
return;
}
this.activity.executionState = this.currentStatusKey;
this.$emit('updateActivityState', {
key: this.activity.id,
executionState: this.currentStatusKey
});
}
}
};
</script>

View File

@@ -21,23 +21,23 @@
-->
<template>
<div v-if="timeProperties.length" class="u-contents">
<div class="c-inspect-properties__header">
{{ heading }}
<div>
<div v-if="timeProperties.length" class="u-contents">
<div class="c-inspect-properties__header">
{{ heading }}
</div>
<ul
v-for="timeProperty in timeProperties"
:key="timeProperty.id"
class="c-inspect-properties__section"
>
<activity-property :label="timeProperty.label" :value="timeProperty.value" />
</ul>
</div>
<ul
v-for="timeProperty in timeProperties"
:key="timeProperty.id"
class="c-inspect-properties__section"
>
<activity-property :label="timeProperty.label" :value="timeProperty.value" />
</ul>
</div>
</template>
<script>
import { v4 as uuid } from 'uuid';
import ActivityProperty from './ActivityProperty.vue';
export default {
@@ -64,13 +64,14 @@ export default {
},
methods: {
setProperties() {
Object.keys(this.activity).forEach((key) => {
if (this.activity[key].label) {
const label = this.activity[key].label;
const value = String(this.activity[key].value);
Object.keys(this.activity.timeProperties).forEach((key) => {
if (this.activity.timeProperties[key].label) {
const label = this.activity.timeProperties[key].label;
const value = String(this.activity.timeProperties[key].value);
const id = this.activity.id;
this.timeProperties[this.timeProperties.length] = {
id: uuid(),
id,
label,
value
};

View File

@@ -45,6 +45,10 @@ export function getValidatedData(domainObject) {
groupActivity.end = activity[sourceMap.end];
}
if (sourceMap.id) {
groupActivity.id = activity[sourceMap.id];
}
if (!mappedJson[groupIdKey]) {
mappedJson[groupIdKey] = [];
}

View File

@@ -27,6 +27,7 @@ import ExampleUser from '../../example/exampleUser/plugin.js';
import ExampleFaultSource from '../../example/faultManagement/exampleFaultSource.js';
import GeneratorPlugin from '../../example/generator/plugin.js';
import ExampleImagery from '../../example/imagery/plugin.js';
import ActivityStatesPlugin from './activityStates/plugin.js';
import AutoflowPlugin from './autoflow/AutoflowTabularPlugin.js';
import BarChartPlugin from './charts/bar/plugin.js';
import ScatterPlotPlugin from './charts/scatter/plugin.js';
@@ -101,6 +102,7 @@ plugins.LocalTimeSystem = LocalTimeSystem;
plugins.RemoteClock = RemoteClock;
plugins.MyItems = MyItems;
plugins.ActivityStates = ActivityStatesPlugin;
plugins.StaticRootPlugin = StaticRootPlugin;

View File

@@ -0,0 +1,112 @@
<!--
Open MCT, Copyright (c) 2014-2024, 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.
-->
<template>
<compact-view-item
v-for="item in sortedItems"
:key="item.key"
:item="item"
:item-properties="itemProperties"
>
</compact-view-item>
</template>
<script>
import _ from 'lodash';
import CompactViewItem from './CompactViewItem.vue';
export default {
components: { CompactViewItem },
inject: ['domainObject', 'openmct'],
props: {
headerItems: {
type: Array,
required: true
},
items: {
type: Array,
required: true
},
defaultSort: {
type: Object,
default() {
return {
property: '',
defaultDirection: true
};
}
}
},
data() {
let sortBy = this.defaultSort.property;
let ascending = this.defaultSort.defaultDirection;
return {
sortBy,
ascending
};
},
computed: {
sortedItems() {
let sortedItems = _.sortBy(this.items, this.sortBy);
if (!this.ascending) {
sortedItems = sortedItems.reverse();
}
return sortedItems;
},
itemProperties() {
return this.headerItems.map((headerItem) => {
return {
property: headerItem.property,
format: headerItem.format
};
});
}
},
watch: {
defaultSort: {
handler() {
this.setSort();
},
deep: true
}
},
methods: {
setSort() {
this.sortBy = this.defaultSort.property;
this.ascending = this.defaultSort.defaultDirection;
},
sort(data) {
const property = data.property;
const direction = data.direction;
if (this.sortBy === property) {
this.ascending = !this.ascending;
} else {
this.sortBy = property;
this.ascending = direction;
}
}
}
};
</script>

View File

@@ -0,0 +1,134 @@
<!--
Open MCT, Copyright (c) 2014-2024, 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.
-->
<template>
<div :class="listItemClass">
<div class="c-tli__activity-color">
<div class="c-tli__activity-color-swatch" :style="styleClass"></div>
</div>
<div class="c-tli__title-and-bounds">
<div class="c-tli__title">{{ formattedItemValue.title }}</div>
<div class="c-tli__bounds">
<span class="c-tli__duration">{{ formattedItemValue.duration }}</span>
<span class="c-tli__start-time">{{ formattedItemValue.start }}</span>
<span class="c-tli__end-time">{{ formattedItemValue.end }}</span>
</div>
</div>
<div class="c-tli__progress-pie">
<svg viewBox="0 0 100 100">
<circle class="c-svg-progress__bg" r="50" cx="50" cy="50"></circle>
<path id="svg-progress-path" class="c-svg-progress__progress"></path>
<circle
class="c-svg-progress__ticks"
r="40"
cx="50"
cy="50"
stroke-dasharray="3 7.472"
></circle>
</svg>
</div>
<div class="c-tli__time-hero">
<div class="c-tli__time-hero-context">{{ formattedItemValue.label }}</div>
<div class="c-tli__time-hero-time --is-countdown">{{ formattedItemValue.countdown }}</div>
</div>
</div>
</template>
<script>
const CURRENT_CSS_SUFFIX = '--is-current';
const PAST_CSS_SUFFIX = '--is-past';
const FUTURE_CSS_SUFFIX = '--is-future';
const ITEM_COLORS = {
[CURRENT_CSS_SUFFIX]: '#ffcc00',
[PAST_CSS_SUFFIX]: '#0088ff',
[FUTURE_CSS_SUFFIX]: '#7300ff'
};
const ITEM_STATES = {
notStarted: 'not-started',
inProgress: 'in-progress',
completed: 'completed',
aborted: 'aborted',
skipped: 'skipped',
incomplete: 'incomplete',
overdue: 'overdue',
runningLong: 'running-long'
};
export default {
inject: ['openmct'],
props: {
item: {
type: Object,
required: true
},
itemProperties: {
type: Array,
required: true
}
},
data() {
return {
itemState: ITEM_STATES.notStarted
};
},
computed: {
styleClass() {
return { backgroundColor: ITEM_COLORS[this.item.cssClass] };
},
listItemClass() {
const timeRelationClass = this.item.cssClass;
const itemStateClass = `--is-${this.itemState}`;
return `c-tli ${timeRelationClass} ${itemStateClass}`;
},
formattedItemValue() {
let itemValue = {
title: this.item.name
};
this.itemProperties.forEach((itemProperty) => {
let value = this.item[itemProperty.property];
let formattedValue;
if (itemProperty.format) {
formattedValue = itemProperty.format(
value,
this.item,
itemProperty.property,
this.openmct,
{
skipPrefix: true
}
);
}
itemValue[itemProperty.property] = formattedValue;
let label;
if (itemProperty.property === 'countdown') {
label = value > 0 ? 'Starts' : 'Ended';
}
itemValue.label = itemValue.label ?? label;
});
return itemValue;
}
}
};
</script>

View File

@@ -21,12 +21,20 @@
-->
<template>
<div ref="timelistHolder" class="c-timelist">
<div ref="timelistHolder" :class="listTypeClass">
<compact-view
v-if="isCompact"
:items="planActivities"
:header-items="headerItems"
:default-sort="defaultSort"
></compact-view>
<list-view
v-else
:items="planActivities"
:header-items="headerItems"
:default-sort="defaultSort"
class="sticky"
@item-selection-changed="setSelectionForActivity"
/>
</div>
</template>
@@ -39,6 +47,7 @@ import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
import ListView from '../../ui/components/List/ListView.vue';
import { getPreciseDuration } from '../../utils/duration.js';
import { getValidatedData, getValidatedGroups } from '../plan/util.js';
import CompactView from './CompactView.vue';
import { SORT_ORDER_OPTIONS } from './constants.js';
const SCROLL_TIMEOUT = 10000;
@@ -72,17 +81,22 @@ const headerItems = [
},
{
defaultDirection: false,
property: 'duration',
property: 'countdown',
name: 'Time To/From',
format: function (value) {
format: function (value, object, key, openmct, options = {}) {
let result;
if (value < 0) {
result = `+${getPreciseDuration(Math.abs(value), {
const prefix = options.skipPrefix ? '' : '+';
result = `${prefix}${getPreciseDuration(Math.abs(value), {
excludeMilliSeconds: true,
useDayFormat: true
})}`;
} else if (value > 0) {
result = `-${getPreciseDuration(value, { excludeMilliSeconds: true, useDayFormat: true })}`;
const prefix = options.skipPrefix ? '' : '+';
result = `${prefix}${getPreciseDuration(value, {
excludeMilliSeconds: true,
useDayFormat: true
})}`;
} else {
result = 'Now';
}
@@ -90,6 +104,14 @@ const headerItems = [
return result;
}
},
{
defaultDirection: false,
property: 'duration',
name: 'Duration',
format: function (value, object, key, openmct) {
return `${getPreciseDuration(value, { excludeMilliSeconds: true, useDayFormat: true })}`;
}
},
{
defaultDirection: true,
property: 'name',
@@ -104,6 +126,7 @@ const defaultSort = {
export default {
components: {
CompactView,
ListView
},
inject: ['openmct', 'domainObject', 'path', 'composition'],
@@ -114,9 +137,18 @@ export default {
height: 0,
planActivities: [],
headerItems: headerItems,
defaultSort: defaultSort
defaultSort: defaultSort,
isCompact: false
};
},
computed: {
listTypeClass() {
if (this.isCompact) {
return 'c-timelist c-timelist--large';
}
return 'c-timelist';
}
},
mounted() {
this.isEditing = this.openmct.editor.isEditing();
this.updateTimestamp = _.throttle(this.updateTimestamp, 1000);
@@ -208,15 +240,14 @@ export default {
this.setViewFromConfig(mutatedObject.configuration);
},
setViewFromConfig(configuration) {
this.filterValue = configuration.filter;
if (this.isEditing) {
this.filterValue = configuration.filter;
this.hideAll = false;
this.listActivities();
} else {
this.filterValue = configuration.filter;
this.setSort();
this.listActivities();
this.isCompact = configuration.isCompact;
}
this.listActivities();
},
updateTimestamp(timestamp) {
//The clock never stops ticking
@@ -379,11 +410,13 @@ export default {
activity.key = uuid();
}
activity.duration = activity.end - activity.start;
if (activity.start < this.timestamp) {
//if the activity start time has passed, display the time to the end of the activity
activity.duration = activity.end - this.timestamp;
activity.countdown = activity.end - this.timestamp;
} else {
activity.duration = activity.start - this.timestamp;
activity.countdown = activity.start - this.timestamp;
}
return activity;
@@ -426,7 +459,7 @@ export default {
},
setScrollTop() {
//The view isn't ready yet
if (!this.$el.parentElement) {
if (!this.$el.parentElement || this.isCompact) {
return;
}
@@ -516,6 +549,29 @@ export default {
setEditState(isEditing) {
this.isEditing = isEditing;
this.setViewFromConfig(this.domainObject.configuration);
},
setSelectionForActivity(activity, element) {
const multiSelect = false;
this.openmct.selection.select(
[
{
element: element,
context: {
type: 'activity',
activity: activity
}
},
{
element: this.openmct.layout.$refs.browseObject.$el,
context: {
item: this.domainObject,
supportsMultiSelect: false
}
}
],
multiSelect
);
}
}
};

View File

@@ -30,6 +30,15 @@
These settings don't affect the view while editing, but will be applied after editing is
finished.
</div>
<div class="c-inspect-properties__label" title="Compact view.">Compact View</div>
<div v-if="canEdit" class="c-inspect-properties__value">
<input v-model="isCompact" type="checkbox" @change="updateCompactView()" />
</div>
<div v-else class="c-inspect-properties__value">
{{ isCompact ? 'Enabled' : 'Disabled' }}
</div>
</li>
<li class="c-inspect-properties__row">
<div class="c-inspect-properties__label" title="Sort order of the timelist.">
Sort Order
</div>
@@ -89,7 +98,8 @@ export default {
sortOrderIndex: this.domainObject.configuration.sortOrderIndex,
sortOrderOptions: SORT_ORDER_OPTIONS,
eventTypes: EVENT_TYPES,
isEditing: this.openmct.editor.isEditing()
isEditing: this.openmct.editor.isEditing(),
isCompact: this.domainObject.configuration.isCompact
};
},
computed: {
@@ -117,6 +127,9 @@ export default {
const key = data.property;
const value = data.value;
this.updateProperty(key, value);
},
updateCompactView() {
this.updateProperty('isCompact', this.isCompact);
}
}
};

View File

@@ -19,6 +19,9 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
$c: #ccc;
$cr: 3px;
$interiorMargin: 5px;
.c-timelist {
& .nowMarker.hasCurrent {
@@ -57,3 +60,97 @@
}
}
}
.c-timelist--large {
$textSm: 0.8em;
$textLg: 1.3em;
.c-tli {
background: #444;
border-radius: $cr;
display: grid;
padding: $interiorMargin;
grid-template-columns: min-content 2fr 40px 1fr;
grid-column-gap: $interiorMargin;
&__activity-color {
align-items: start;
display: flex;
padding-top: 3px;
}
&__activity-color-swatch {
$d: 16px;
border-radius: 50%;
width: $d;
height: $d;
}
&__bounds {
font-size: $textSm;
}
&__title {
font-size: $textLg;
}
&__time-hero {
//background: rgba(deeppink, 0.2);
flex-wrap: wrap;
display: flex;
align-items: center;
justify-content: end;
> * + * {
margin-left: $interiorMargin;
}
}
&__time-hero-context {
font-size: $textSm;
text-transform: uppercase;
}
&__time-hero-time {
font-size: $textLg;
&:before {
display: inline-block;
margin-right: 3px;
}
&.--is-countdown {
&:before {
content: '-'; // TODO: replace with symbolsfont dash
}
}
&.--is-countup {
&:before {
content: '+'; // TODO: replace with symbolsfont dash
}
}
}
}
}
.c-svg-progress {
&__bg {
fill: rgba(black, 0.2);
}
&__ticks {
fill: none;
stroke: #666;
stroke-width: 6;
}
&__progress {
fill: $c;
}
}

View File

@@ -43,6 +43,7 @@
:key="item.key"
:item="item"
:item-properties="itemProperties"
@click="itemSelected(item, $event)"
/>
</tbody>
</table>
@@ -86,6 +87,7 @@ export default {
}
}
},
emits: ['itemSelectionChanged'],
data() {
let sortBy = this.defaultSort.property;
let ascending = this.defaultSort.defaultDirection;
@@ -156,6 +158,10 @@ export default {
})
);
}
},
itemSelected(item, event) {
event.stopPropagation();
this.$emit('itemSelectionChanged', item, event.currentTarget);
}
}
};