Compare commits

...

9 Commits

Author SHA1 Message Date
3c60b0416f Fix version 2.2.1 (#6551) 2023-04-03 07:40:13 -07:00
4d93907d58 chore(deps-dev): bump eslint-plugin-compat from 4.1.1 to 4.1.2 (#6352)
Bumps [eslint-plugin-compat](https://github.com/amilajack/eslint-plugin-compat) from 4.1.1 to 4.1.2.
- [Release notes](https://github.com/amilajack/eslint-plugin-compat/releases)
- [Changelog](https://github.com/amilajack/eslint-plugin-compat/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amilajack/eslint-plugin-compat/compare/v4.1.1...v4.1.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-compat
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-03-30 15:37:59 -07:00
6f656a6783 [Build]Remove Node14LTS from supported versions and update our pipelines (#6527)
* bump minimumum and pin drivebys

* move to 16 and remove 14

* remove 14

* update to latest
2023-03-30 20:53:44 +00:00
767fb6c5fd fix: remove redundant request on FaultManagement mount (#6502)
* fix: remove redundant update request

* fix: handle case where request returns no faults

* test: fix fault management tests

* docs: clean up FaultManagement API types

---------

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2023-03-30 18:43:55 +00:00
b0a0b4bb58 chore(deps-dev): bump eslint from 8.36.0 to 8.37.0 (#6521)
Bumps [eslint](https://github.com/eslint/eslint) from 8.36.0 to 8.37.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.36.0...v8.37.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-03-30 11:15:36 -07:00
340f4a9e79 chore(deps-dev): bump @percy/cli from 1.17.0 to 1.21.0 (#6439)
Bumps [@percy/cli](https://github.com/percy/cli/tree/HEAD/packages/cli) from 1.17.0 to 1.21.0.
- [Release notes](https://github.com/percy/cli/releases)
- [Commits](https://github.com/percy/cli/commits/v1.21.0/packages/cli)

---
updated-dependencies:
- dependency-name: "@percy/cli"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-03-30 11:05:32 -07:00
3007b28b0f Simple text export of Notebook (#6510)
* add simple prototype

* tags and metadata now exported

* add form for options

* revert notebook

* add simple e2e test

* add test stubs

* death to debug
2023-03-30 19:44:12 +02:00
20789601b4 Add contextual domain object back for contextual row actions (#6524)
* re-enable historical row action
2023-03-30 10:08:09 -07:00
a56cfed732 Remove ticker and rely solely on the clock ticks to update the timelist durations (#6495)
* Remove ticker for timelist and rename a function for readability

* Use formatting for remote clock if available.

* throttle updates to the timestamp and listing activities

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2023-03-29 15:16:52 -07:00
15 changed files with 305 additions and 89 deletions

View File

@ -175,11 +175,11 @@ workflows:
overall-circleci-commit-status: #These jobs run on every commit
jobs:
- lint:
name: node14-lint
node-version: lts/fermium
name: node16-lint
node-version: lts/gallium
- unit-test:
name: node18-chrome
node-version: "18"
node-version: lts/hydrogen
- e2e-test:
name: e2e-stable
node-version: lts/gallium
@ -191,15 +191,12 @@ workflows:
the-nightly: #These jobs do not run on PRs, but against master at night
jobs:
- unit-test:
name: node14-chrome-nightly
node-version: lts/fermium
- unit-test:
name: node16-chrome-nightly
node-version: lts/gallium
- unit-test:
name: node18-chrome
node-version: "18"
node-version: lts/hydrogen
- npm-audit:
node-version: lts/gallium
- e2e-test:

View File

@ -16,7 +16,6 @@ jobs:
- macos-latest
- windows-latest
node_version:
- 14
- 16
- 18
architecture:

View File

@ -22,6 +22,7 @@
const { test, expect } = require('../../../../pluginFixtures');
const utils = require('../../../../helper/faultUtils');
const { selectInspectorTab } = require('../../../../appActions');
test.describe('The Fault Management Plugin using example faults', () => {
test.beforeEach(async ({ page }) => {
@ -38,6 +39,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector @unstable', async ({ page }) => {
await utils.selectFaultItem(page, 1);
await selectInspectorTab(page, 'Fault Management Configuration');
const selectedFaultName = await page.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname').textContent();
const inspectorFaultNameCount = await page.locator(`.c-inspector__properties >> :text("${selectedFaultName}")`).count();
@ -52,6 +54,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
const selectedRows = page.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname');
expect.soft(await selectedRows.count()).toEqual(2);
await selectInspectorTab(page, 'Fault Management Configuration');
const firstSelectedFaultName = await selectedRows.nth(0).textContent();
const secondSelectedFaultName = await selectedRows.nth(1).textContent();
const firstNameInInspectorCount = await page.locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`).count();

View File

@ -377,4 +377,31 @@ test.describe('Notebook entry tests', () => {
expect.soft(await sanitizedLink.count()).toBe(1);
expect(await unsanitizedLink.count()).toBe(0);
});
test('can export notebook as text', async ({ page }) => {
await nbUtils.enterTextEntry(page, `Foo bar entry`);
// Click on 3 Dot Menu
await page.locator('button[title="More options"]').click();
const downloadPromise = page.waitForEvent('download');
await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click();
await page.getByRole('button', { name: 'Save' }).click();
const download = await downloadPromise;
const readStream = await download.createReadStream();
const exportedText = await streamToString(readStream);
expect(exportedText).toContain('Foo bar entry');
});
test.fixme('can export multiple notebook entries as text ', async ({ page }) => {});
test.fixme('can export all notebook entry metdata', async ({ page }) => {});
test.fixme('can export all notebook tags', async ({ page }) => {});
test.fixme('can export all notebook snapshots', async ({ page }) => {});
async function streamToString(readable) {
let result = '';
for await (const chunk of readable) {
result += chunk;
}
return result;
}
});

View File

@ -33,6 +33,8 @@ export default function (staticFaults = false) {
return Promise.resolve(faultsData);
},
subscribe(domainObject, callback) {
callback({ type: 'global-alarm-status' });
return () => {};
},
supportsRequest(domainObject) {

View File

@ -1,11 +1,11 @@
{
"name": "openmct",
"version": "2.2.1-SNAPSHOT",
"version": "2.2.1",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.2",
"@percy/cli": "1.17.0",
"@percy/cli": "1.21.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.29.0",
"@types/eventemitter3": "1.2.0",
@ -20,8 +20,8 @@
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.36.0",
"eslint-plugin-compat": "4.1.1",
"eslint": "8.37.0",
"eslint-plugin-compat": "4.1.2",
"eslint-plugin-playwright": "0.12.0",
"eslint-plugin-vue": "9.10.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
@ -41,7 +41,7 @@
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.36",
"karma-webpack": "5.0.0",
"kdbush": "^3.0.0",
"kdbush": "3.0.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.7.5",
@ -59,7 +59,7 @@
"sass": "1.59.3",
"sass-loader": "13.2.1",
"sinon": "15.0.1",
"style-loader": "^3.3.1",
"style-loader": "3.3.2",
"typescript": "4.9.5",
"uuid": "9.0.0",
"vue": "2.6.14",
@ -107,7 +107,7 @@
"url": "https://github.com/nasa/openmct.git"
},
"engines": {
"node": ">=14.19.1"
"node": ">=16.20.0"
},
"browserslist": [
"Firefox ESR",

View File

@ -21,18 +21,31 @@
*****************************************************************************/
export default class FaultManagementAPI {
/**
* @param {import("openmct").OpenMCT} openmct
*/
constructor(openmct) {
this.openmct = openmct;
}
/**
* @param {*} provider
*/
addProvider(provider) {
this.provider = provider;
}
/**
* @returns {boolean}
*/
supportsActions() {
return this.provider?.acknowledgeFault !== undefined && this.provider?.shelveFault !== undefined;
}
/**
* @param {import("../objects/ObjectAPI").DomainObject} domainObject
* @returns {Promise.<FaultAPIResponse[]>}
*/
request(domainObject) {
if (!this.provider?.supportsRequest(domainObject)) {
return Promise.reject();
@ -41,6 +54,11 @@ export default class FaultManagementAPI {
return this.provider.request(domainObject);
}
/**
* @param {import("../objects/ObjectAPI").DomainObject} domainObject
* @param {Function} callback
* @returns {Function} unsubscribe
*/
subscribe(domainObject, callback) {
if (!this.provider?.supportsSubscribe(domainObject)) {
return Promise.reject();
@ -49,58 +67,55 @@ export default class FaultManagementAPI {
return this.provider.subscribe(domainObject, callback);
}
/**
* @param {Fault} fault
* @param {*} ackData
*/
acknowledgeFault(fault, ackData) {
return this.provider.acknowledgeFault(fault, ackData);
}
/**
* @param {Fault} fault
* @param {*} shelveData
* @returns {Promise.<T>}
*/
shelveFault(fault, shelveData) {
return this.provider.shelveFault(fault, shelveData);
}
}
/** @typedef {object} Fault
* @property {string} type
* @property {object} fault
* @property {boolean} fault.acknowledged
* @property {object} fault.currentValueInfo
* @property {number} fault.currentValueInfo.value
* @property {string} fault.currentValueInfo.rangeCondition
* @property {string} fault.currentValueInfo.monitoringResult
* @property {string} fault.id
* @property {string} fault.name
* @property {string} fault.namespace
* @property {number} fault.seqNum
* @property {string} fault.severity
* @property {boolean} fault.shelved
* @property {string} fault.shortDescription
* @property {string} fault.triggerTime
* @property {object} fault.triggerValueInfo
* @property {number} fault.triggerValueInfo.value
* @property {string} fault.triggerValueInfo.rangeCondition
* @property {string} fault.triggerValueInfo.monitoringResult
* @example
* {
* "type": "",
* "fault": {
* "acknowledged": true,
* "currentValueInfo": {
* "value": 0,
* "rangeCondition": "",
* "monitoringResult": ""
* },
* "id": "",
* "name": "",
* "namespace": "",
* "seqNum": 0,
* "severity": "",
* "shelved": true,
* "shortDescription": "",
* "triggerTime": "",
* "triggerValueInfo": {
* "value": 0,
* "rangeCondition": "",
* "monitoringResult": ""
* }
* }
* }
/**
* @typedef {object} TriggerValueInfo
* @property {number} value
* @property {string} rangeCondition
* @property {string} monitoringResult
*/
/**
* @typedef {object} CurrentValueInfo
* @property {number} value
* @property {string} rangeCondition
* @property {string} monitoringResult
*/
/**
* @typedef {object} Fault
* @property {boolean} acknowledged
* @property {CurrentValueInfo} currentValueInfo
* @property {string} id
* @property {string} name
* @property {string} namespace
* @property {number} seqNum
* @property {string} severity
* @property {boolean} shelved
* @property {string} shortDescription
* @property {string} triggerTime
* @property {TriggerValueInfo} triggerValueInfo
*/
/**
* @typedef {object} FaultAPIResponse
* @property {string} type
* @property {Fault} fault
*/

View File

@ -42,8 +42,6 @@ export default {
};
},
mounted() {
this.updateFaultList();
this.unsubscribe = this.openmct.faults
.subscribe(this.domainObject, this.updateFault);
},
@ -68,7 +66,11 @@ export default {
this.openmct.faults
.request(this.domainObject)
.then(faultsData => {
this.faultsList = faultsData.map(fd => fd.fault);
if (faultsData?.length > 0) {
this.faultsList = faultsData.map(fd => fd.fault);
} else {
this.faultsList = [];
}
});
}
}

View File

@ -0,0 +1,155 @@
import {saveAs} from 'saveAs';
import Moment from 'moment';
const UNKNOWN_USER = 'Unknown';
const UNKNOWN_TIME = 'Unknown';
export default class ExportNotebookAsTextAction {
constructor(openmct) {
this.openmct = openmct;
this.cssClass = 'icon-export';
this.description = 'Exports notebook contents as a text file';
this.group = "action";
this.key = 'exportNotebookAsText';
this.name = 'Export Notebook as Text';
this.priority = 1;
}
invoke(objectPath) {
this.showForm(objectPath);
}
getTagName(tagId, availableTags) {
const foundTag = availableTags.find(tag => tag.id === tagId);
if (foundTag) {
return foundTag.label;
} else {
return tagId;
}
}
getTagsForEntry(entry, domainObjectKeyString, annotations) {
const foundTags = [];
annotations.forEach(annotation => {
const target = annotation.targets?.[domainObjectKeyString];
if (target?.entryId === entry.id) {
annotation.tags.forEach(tag => {
if (!foundTags.includes(tag)) {
foundTags.push(tag);
}
});
}
});
return foundTags;
}
formatTimeStamp(timestamp) {
if (timestamp) {
return `${Moment.utc(timestamp).format('YYYY-MM-DD HH:mm:ss')} UTC`;
} else {
return UNKNOWN_TIME;
}
}
appliesTo(objectPath) {
const domainObject = objectPath[0];
const type = this.openmct.types.get(domainObject.type);
return type?.definition?.name === 'Notebook';
}
async onSave(changes, objectPath) {
const availableTags = this.openmct.annotation.getAvailableTags();
const identifier = objectPath[0].identifier;
const domainObject = await this.openmct.objects.get(identifier);
let foundAnnotations = [];
// only load annotations if there are tags
if (availableTags.length) {
foundAnnotations = await this.openmct.annotation.getAnnotations(domainObject.identifier);
}
let notebookAsText = `# ${domainObject.name}\n\n`;
if (changes.exportMetaData) {
const createdTimestamp = domainObject.created;
const createdBy = domainObject.createdBy ?? UNKNOWN_USER;
const modifiedBy = domainObject.modifiedBy ?? UNKNOWN_USER;
const modifiedTimestamp = domainObject.modified ?? domainObject.created;
notebookAsText += `Created on ${this.formatTimeStamp(createdTimestamp)} by user ${createdBy}\n\n`;
notebookAsText += `Updated on ${this.formatTimeStamp(modifiedTimestamp)} by user ${modifiedBy}\n\n`;
}
const notebookSections = domainObject.configuration.sections;
const notebookEntries = domainObject.configuration.entries;
notebookSections.forEach(section => {
notebookAsText += `## ${section.name}\n\n`;
const notebookPages = section.pages;
notebookPages.forEach(page => {
notebookAsText += `### ${page.name}\n\n`;
const notebookPageEntries = notebookEntries[section.id]?.[page.id];
notebookPageEntries.forEach(entry => {
if (changes.exportMetaData) {
const createdTimestamp = entry.createdOn;
const createdBy = entry.createdBy ?? UNKNOWN_USER;
const modifiedBy = entry.modifiedBy ?? UNKNOWN_USER;
const modifiedTimestamp = entry.modified ?? entry.created;
notebookAsText += `Created on ${this.formatTimeStamp(createdTimestamp)} by user ${createdBy}\n\n`;
notebookAsText += `Updated on ${this.formatTimeStamp(modifiedTimestamp)} by user ${modifiedBy}\n\n`;
}
if (changes.exportTags) {
const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
const tags = this.getTagsForEntry(entry, domainObjectKeyString, foundAnnotations);
const tagNames = tags.map(tag => this.getTagName(tag, availableTags));
if (tagNames) {
notebookAsText += `Tags: ${tagNames.join(', ')}\n\n`;
}
}
notebookAsText += `${entry.text}\n\n`;
});
});
});
const blob = new Blob([notebookAsText], {type: "text/markdown"});
const fileName = domainObject.name + '.md';
saveAs(blob, fileName);
}
async showForm(objectPath) {
const formStructure = {
title: "Export Notebook Text",
sections: [
{
rows: [
{
key: "exportMetaData",
control: "toggleSwitch",
name: "Include Metadata (created/modified, etc.)",
required: true,
value: false
},
{
name: "Include Tags",
control: "toggleSwitch",
required: true,
key: 'exportTags',
value: false
}
]
}
]
};
const changes = await this.openmct.forms.showForm(formStructure);
return this.onSave(changes, objectPath);
}
}

View File

@ -21,6 +21,7 @@
*****************************************************************************/
import CopyToNotebookAction from './actions/CopyToNotebookAction';
import ExportNotebookAsTextAction from './actions/ExportNotebookAsTextAction';
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
import NotebookViewProvider from './NotebookViewProvider';
import NotebookType from './NotebookType';
@ -80,6 +81,7 @@ function installBaseNotebookFunctionality(openmct) {
};
openmct.types.addType('notebookSnapshotImage', notebookSnapshotImageType);
openmct.actions.register(new CopyToNotebookAction(openmct));
openmct.actions.register(new ExportNotebookAsTextAction(openmct));
const notebookSnapshotIndicator = new Vue ({
components: {

View File

@ -46,6 +46,7 @@ export default class RemoteClock extends DefaultClock {
this.timeTelemetryObject = undefined;
this.parseTime = undefined;
this.formatTime = undefined;
this.metadata = undefined;
this.lastTick = 0;
@ -137,6 +138,10 @@ export default class RemoteClock extends DefaultClock {
this.parseTime = (datum) => {
return timeFormatter.parse(datum);
};
this.formatTime = (datum) => {
return timeFormatter.format(datum);
};
}
/**

View File

@ -88,7 +88,7 @@ define([], function () {
}
getContextMenuActions() {
return ['viewDatumAction'];
return ['viewDatumAction', 'viewHistoricalData'];
}
}

View File

@ -175,14 +175,22 @@ export default {
getDatum() {
return this.row.fullDatum;
},
showContextMenu: function (event) {
showContextMenu: async function (event) {
event.preventDefault();
this.updateViewContext();
this.markRow(event);
const contextualDomainObject = await this.row.getContextualDomainObject?.(this.openmct, this.row.objectKeyString);
let objectPath = this.objectPath;
if (contextualDomainObject) {
objectPath = objectPath.slice();
objectPath.unshift(contextualDomainObject);
}
const actions = this.row.getContextMenuActions().map(key => this.openmct.actions.getAction(key));
const menuItems = this.openmct.menus.actionsToMenuItems(actions, this.objectPath, this.currentView);
const menuItems = this.openmct.menus.actionsToMenuItems(actions, objectPath, this.currentView);
if (menuItems.length) {
this.openmct.menus.showMenu(event.x, event.y, menuItems);
}

View File

@ -38,9 +38,8 @@
import {getValidatedData} from "../plan/util";
import ListView from '../../ui/components/List/ListView.vue';
import {getPreciseDuration} from "../../utils/duration";
import ticker from 'utils/clock/Ticker';
import {SORT_ORDER_OPTIONS} from "./constants";
import _ from 'lodash';
import moment from "moment";
import { v4 as uuid } from 'uuid';
@ -53,16 +52,26 @@ const headerItems = [
isSortable: true,
property: 'start',
name: 'Start Time',
format: function (value, object) {
return `${moment(value).format(TIME_FORMAT)}Z`;
format: function (value, object, key, openmct) {
const clock = openmct.time.clock();
if (clock && clock.formatTime) {
return clock.formatTime(value);
} else {
return `${moment(value).format(TIME_FORMAT)}Z`;
}
}
}, {
defaultDirection: true,
isSortable: true,
property: 'end',
name: 'End Time',
format: function (value, object) {
return `${moment(value).format(TIME_FORMAT)}Z`;
format: function (value, object, key, openmct) {
const clock = openmct.time.clock();
if (clock && clock.formatTime) {
return clock.formatTime(value);
} else {
return `${moment(value).format(TIME_FORMAT)}Z`;
}
}
}, {
defaultDirection: false,
@ -119,7 +128,8 @@ export default {
this.unlistenConfig = this.openmct.objects.observe(this.domainObject, 'configuration', this.setViewFromConfig);
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
this.status = this.openmct.status.get(this.domainObject.identifier);
this.unlistenTicker = ticker.listen(this.clearPreviousActivities);
this.updateTimestamp = _.throttle(this.updateTimestamp, 1000);
this.openmct.time.on('bounds', this.updateTimestamp);
this.openmct.editor.on('isEditing', this.setEditState);
@ -144,10 +154,6 @@ export default {
this.unlistenConfig();
}
if (this.unlistenTicker) {
this.unlistenTicker();
}
if (this.removeStatusListener) {
this.removeStatusListener();
}
@ -192,8 +198,8 @@ export default {
}
},
updateTimestamp(_bounds, isTick) {
if (isTick === true) {
this.timestamp = this.openmct.time.clock().currentValue();
if (isTick === true && this.openmct.time.clock() !== undefined) {
this.updateTimeStampAndListActivities(this.openmct.time.clock().currentValue());
}
},
setViewFromClock(newClock) {
@ -202,12 +208,11 @@ export default {
if (isFixedTime) {
this.hideAll = false;
this.showAll = true;
// clear invokes listActivities
this.clearPreviousActivities(this.openmct.time.bounds()?.start);
this.updateTimeStampAndListActivities(this.openmct.time.bounds()?.start);
} else {
this.setSort();
this.setViewBounds();
this.listActivities();
this.updateTimeStampAndListActivities(this.openmct.time.clock().currentValue());
}
},
addItem(domainObject) {
@ -346,12 +351,8 @@ export default {
// sort by start time
this.planActivities = activities.sort(this.sortByStartTime);
},
clearPreviousActivities(time) {
if (time instanceof Date) {
this.timestamp = time.getTime();
} else {
this.timestamp = time;
}
updateTimeStampAndListActivities(time) {
this.timestamp = time;
this.listActivities();
},

View File

@ -37,7 +37,7 @@ export default {
// eslint-disable-next-line you-dont-need-lodash-underscore/get
let value = _.get(this.item, property.key);
if (property.format) {
value = property.format(value, this.item, property.key);
value = property.format(value, this.item, property.key, this.openmct);
}
values.push({