Compare commits

..

41 Commits

Author SHA1 Message Date
Joe Pea
6a180a4b2c add an initial manually-created visual snapshot test of log plot chart 2022-04-05 20:16:39 -07:00
Joe Pea
cab32e1a30 test log ticks work after refresh 2022-04-05 14:59:31 -07:00
Joe Pea
35465961da add log plot tests 2022-04-05 10:15:53 -07:00
Shefali Joshi
55731a4653 Merge branch 'master' into log-plots-2/custom-ticks 2022-04-04 09:12:50 -07:00
Joe Pea
3f9ebc5960 fix incorrect logMode state during initialization 2022-03-30 21:28:12 -07:00
Joe Pea
3c31fe7baa Merge branch 'master' into log-plots-2/custom-ticks 2022-03-30 15:32:52 -07:00
Shefali Joshi
1f1e2a9b1a Merge branch 'master' into log-plots-2/custom-ticks 2022-03-30 10:48:22 -07:00
Joe Pea
36a6786947 Merge branch 'master' into log-plots-2/custom-ticks 2022-03-29 18:42:07 -07:00
Joe Pea
e2eddbb537 Merge branch 'type-annotations' into log-plots-2/custom-ticks 2022-03-28 15:35:35 -07:00
Joe Pea
a6d86d470f Merge branch 'master' into type-annotations 2022-03-28 15:14:34 -07:00
Joe Pea
6a01ce0c2d lint fix 2022-03-28 11:16:05 -07:00
John Hill
064fa80fdc Merge branch 'master' into type-annotations 2022-03-25 14:00:11 -07:00
John Hill
08e84c9ad3 Merge branch 'master' into type-annotations 2022-03-22 17:50:23 -07:00
Joe Pea
f5d4e75c52 undo the changes to MctChart, improve it later 2022-03-22 11:34:14 -07:00
Joe Pea
5f816179d6 Merge branch 'master' into type-annotations 2022-03-22 10:37:12 -07:00
Joe Pea
817f8411f1 Merge branch 'master' into type-annotations 2022-03-15 13:02:05 -07:00
Joe Pea
4ba0fbc482 improve plot series reset as per Shefali, and adjust displayRange on logMode change 2022-03-07 16:51:49 -08:00
Joe Pea
d7a44310d4 add log mode status to UI in non-edit mode 2022-03-07 16:35:11 -08:00
Joe Pea
f7a0c030fa Add UI to toggle log mode.
This almost works: ticks don't redraw until a zoom or pan is performed.
2022-02-16 23:39:49 -08:00
John Hill
c87c9f48fd Merge branch 'master' into type-annotations 2022-02-16 14:45:12 -08:00
Joe Pea
0d6de7dfdb Merge branch 'type-annotations' into log-plots 2022-02-15 13:20:17 -08:00
Joe Pea
f05e895e3a update types, avoid runtime behavior in type definition that breaks SeriesCollection 2022-02-15 13:20:03 -08:00
Joe Pea
429ca484ed further simplify plot canvas creation 2022-02-15 12:39:27 -08:00
Joe Pea
56a2e63600 further simplify plot canvas creation 2022-02-15 12:36:56 -08:00
Joe Pea
e6c2a118f7 Merge branch 'type-annotations' into log-plots 2022-02-15 11:31:40 -08:00
Joe Pea
c917914183 Merge branch 'master' into type-annotations 2022-02-15 11:31:28 -08:00
Joe Pea
fb1d6c0187 use a symlog function to cover negative values 2022-02-08 16:24:06 -08:00
Joe Pea
3b42490883 use getLogTicks instead of getLogTicks2. It is currently more useful, especially when zooming out 2022-02-02 20:21:58 -08:00
Joe Pea
5b756d3588 make unevenly spread (but numericaly even) ticks 2022-02-02 16:30:52 -08:00
Joe Pea
63e8fb53f8 add more ticks to log plot axis (needs more work, but it works) 2022-02-02 16:18:53 -08:00
Joe Pea
72aea12f68 Merge branch 'master' into type-annotations 2022-01-31 12:46:22 -08:00
unlikelyzero
c9d96565fa Add karma and jasmine, too 2022-01-27 06:50:56 -08:00
unlikelyzero
fe447a0d4c add mocha types 2022-01-27 06:47:38 -08:00
Joe Pea
d21adc6f69 WIP log plot values and ticks experiment 2022-01-26 12:02:24 -08:00
Joe Pea
607acf9626 Merge branch 'master' into type-annotations 2022-01-26 12:02:00 -08:00
Joe Pea
4251274174 Merge branch 'master' into type-annotations 2022-01-26 11:46:37 -08:00
Joe Pea
ffaeea3d31 more type annotations and small tweaks to make types show 2022-01-26 11:28:11 -08:00
Joe Pea
84693b008e more type annotations and a few small tweaks 2022-01-20 19:19:44 -08:00
Joe Pea
896571e20e some more type defs and small code tweaks while getting familiar with plots 2022-01-11 21:57:08 -08:00
Joe Pea
4ff150caf4 Merge branch 'master' into log-plots 2022-01-11 12:29:36 -08:00
Joe Pea
ddf45a18b0 add some types to XAxisModel 2022-01-11 11:37:02 -08:00
97 changed files with 727 additions and 5173 deletions

View File

@@ -154,10 +154,6 @@ workflows:
name: node16-chrome
node-version: lts/gallium
browser: ChromeHeadless
- unit-test:
name: node18-chrome
node-version: "18"
browser: ChromeHeadless
- e2e-test:
name: e2e-ci
node-version: lts/gallium
@@ -180,10 +176,6 @@ workflows:
name: node16-chrome-nightly
node-version: lts/gallium
browser: ChromeHeadless
- unit-test:
name: node18-chrome
node-version: "18"
browser: ChromeHeadless
- npm-audit:
node-version: lts/gallium
- e2e-test:

View File

@@ -34,7 +34,7 @@ jobs:
- run: npm install
- run: npm run test:e2e:full
- name: Archive test results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
path: test-results
- name: Test success

View File

@@ -18,7 +18,6 @@ jobs:
node_version:
- 14
- 16
- 18
architecture:
- x64
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}

View File

@@ -21,17 +21,13 @@
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
suite is sharing state between tests which is considered an anti-pattern. Implimenting in this way to
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
*/
const { test, expect } = require('@playwright/test');
let conditionSetUrl;
let getConditionSetIdentifierFromUrl;
test('Create new Condition Set object and store @localStorage', async ({ page, context }) => {
test.describe('Condition Set Operations', () => {
test('Create new button `condition set` creates new condition object', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
@@ -43,141 +39,27 @@ test('Create new Condition Set object and store @localStorage', async ({ page, c
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
page.click('text=OK')
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Save localStorage for future test execution
await context.storageState({ path: './e2e/tests/recycled_storage.json' });
//Set object identifier from url
conditionSetUrl = await page.url();
console.log('conditionSetUrl ' + conditionSetUrl);
getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
console.log('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
});
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
//Load localStorage for subsequent tests
test.use({ storageState: './e2e/tests/recycled_storage.json' });
//Begin suite of tests again localStorage
test('Condition set object properties persist in main view and inspector', async ({ page }) => {
//Navigate to baseURL with injected localStorage
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
//Assertions on loaded Condition Set in main view
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
//Reload Page
await Promise.all([
page.reload(),
page.waitForLoadState('networkidle')
]);
//Re-verify after reload
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
});
test('condition set object can be modified on @localStorage', async ({ page }) => {
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
//Assertions on loaded Condition Set in main view
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
test.fixme('condition set object properties exist', async ({ page }) => {
//Go to object created in step one
//Verify the Condition Set properties persist on Save
//Verify the Condition Set properties persist on page.reload()
});
test.fixme('condition set object can be modified', async ({ page }) => {
//Go to object created in step one
//Update the Condition Set properties
// Click Edit Button
await page.locator('text=Conditions View Snapshot >> button').nth(3).click();
//Edit Condition Set Name from main view
await page.locator('text=Unnamed Condition Set').first().fill('Renamed Condition Set');
await page.locator('text=Renamed Condition Set').first().press('Enter');
// Click Save Button
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// Click Save and Finish Editing Option
await page.locator('text=Save and Finish Editing').click();
//Verify Main section reflects updated Name Property
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
// Verify Inspector properties
// Verify Inspector has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
// Verify Inspector Details has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety
// Expand Tree
await page.locator('text=Open MCT My Items >> span >> nth=3').click();
// Verify Condition Set Object is renamed in Tree
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property
await page.locator('input[type="search"]').fill('Renamed');
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
//Reload Page
await Promise.all([
page.reload(),
page.waitForLoadState('networkidle')
]);
//Verify Main section reflects updated Name Property
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
// Verify Inspector properties
// Verify Inspector has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
// Verify Inspector Details has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety
// Expand Tree
await page.locator('text=Open MCT My Items >> span >> nth=3').click();
// Verify Condition Set Object is renamed in Tree
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property
await page.locator('input[type="search"]').fill('Renamed');
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
//Verify the Condition Set properties persist on Save
//Verify the Condition Set properties persist on page.reload()
});
test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
//Navigate to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Expect Unnamed Condition Set to be visible in Main View
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).toBeVisible();
// Search for Unnamed Condition Set
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
// Right Click to Open Actions Menu
await page.locator('a:has-text("Unnamed Condition Set")').click({
button: 'right'
});
// Click Remove Action
await page.locator('text=Remove').click();
await page.locator('text=OK').click();
//Expect Unnamed Condition Set to be removed in Main View
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).not.toBeVisible();
await page.locator('.c-search__clear-input').click();
// Search for Unnamed Condition Set
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
// Expect Unnamed Condition Set to be removed
await expect(page.locator('a:has-text("Unnamed Condition Set")')).not.toBeVisible();
//Feature?
//Domain Object is still available by direct URL after delete
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
test.fixme('condition set object can be deleted', async ({ page }) => {
//Go to object created in step one
//Verify that Condition Set object can be deleted
//Verify the Condition Set object does not exist in Tree
//Verify the Condition Set object does not exist with direct navigation to object's URL
});
});

View File

@@ -1,190 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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.
*****************************************************************************/
/*
Test for plot autoscale.
*/
const { test: _test, expect } = require('@playwright/test');
// create a new `test` API that will not append platform details to snapshot
// file names, only for the tests in this file, so that the same snapshots will
// be used for all platforms.
const test = _test.extend({
_autoSnapshotSuffix: [
async ({}, use, testInfo) => {
testInfo.snapshotSuffix = '';
await use();
},
{ auto: true }
]
});
test.use({
viewport: {
width: 1280,
height: 720
}
});
test.describe('ExportAsJSON', () => {
test('autoscale off causes no error from undefined user range', async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
await setTimeRange(page);
await createSinewaveOverlayPlot(page);
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
await turnOffAutoscale(page);
const canvas = page.locator('canvas').nth(1);
// Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior.
await Promise.all([
testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']),
new Promise(r => setTimeout(r, 100))
.then(() => canvas.screenshot())
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 }))
]);
let errorCount = 0;
function onError() {
errorCount++;
}
page.on('pageerror', onError);
await page.keyboard.down('Alt');
await canvas.dragTo(canvas, {
sourcePosition: {
x: 200,
y: 200
},
targetPosition: {
x: 400,
y: 400
}
});
await page.keyboard.up('Alt');
page.off('pageerror', onError);
// There would have been an error at this point. So if there isn't, then
// we fixed it.
expect(errorCount).toBe(0);
// Ensure the drag worked.
await Promise.all([
testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']),
new Promise(r => setTimeout(r, 100))
.then(() => canvas.screenshot())
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 40 }))
]);
});
});
/**
* @param {import('@playwright/test').Page} page
* @param {string} start
* @param {string} end
*/
async function setTimeRange(page, start = '2022-03-29 22:00:00.000Z', end = '2022-03-29 22:00:30.000Z') {
// Set a specific time range for consistency, otherwise it will change
// on every test to a range based on the current time.
const timeInputs = page.locator('input.c-input--datetime');
await timeInputs.first().click();
await timeInputs.first().fill(start);
await timeInputs.nth(1).click();
await timeInputs.nth(1).fill(end);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function createSinewaveOverlayPlot(page) {
// click create button
await page.locator('button:has-text("Create")').click();
// add overlay plot with defaults
await page.locator('li:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-overlay' }*/),
page.locator('text=OK').click()
]);
// save (exit edit mode)
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
await page.locator('text=Save and Finish Editing').click();
// click create button
await page.locator('button:has-text("Create")').click();
// add sine wave generator with defaults
await page.locator('li:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396/5cfa5c69-17bc-4a99-9545-4da8125380c5?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-single' }*/),
page.locator('text=OK').click()
]);
// focus the overlay plot
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-overlay' }*/),
page.locator('text=Unnamed Overlay Plot').first().click()
]);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function turnOffAutoscale(page) {
// enter edit mode
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
// uncheck autoscale
await page.locator('text=Y Axis Scaling Auto scale Padding >> input[type="checkbox"]').uncheck();
// save
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
await page.locator('text=Save and Finish Editing').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function testYTicks(page, values) {
const yTicks = page.locator('.gl-plot-y-tick-label');
let promises = [yTicks.count().then(c => expect(c).toBe(values.length))];
for (let i = 0, l = values.length; i < l; i += 1) {
promises.push(expect(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line
}
await Promise.all(promises);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,22 +0,0 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1651513945533,\"end\":1651515745533}]}"
},
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1651515746374,\"modified\":1651515746374},\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"e35a066b-eb0e-4b05-a4c9-cc31dc202572\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1651515746373,\"location\":\"mine\",\"persisted\":1651515746373}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
}
]
}
]
}

View File

@@ -195,7 +195,6 @@
));
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
openmct.install(openmct.plugins.Timer());
openmct.install(openmct.plugins.Timelist());
openmct.start();
</script>
</html>

View File

@@ -1,12 +1,12 @@
{
"name": "openmct",
"version": "2.0.3",
"version": "2.0.2-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.16.3",
"@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.0.4",
"@percy/playwright": "1.0.2",
"@percy/cli": "1.0.0-beta.76",
"@percy/playwright": "1.0.1",
"@playwright/test": "1.19.2",
"@types/eventemitter3": "^1.0.0",
"@types/jasmine": "^4.0.1",
@@ -18,12 +18,13 @@
"babel-plugin-istanbul": "6.1.1",
"comma-separated-values": "3.6.4",
"copy-webpack-plugin": "10.2.0",
"core-js": "3.21.1",
"cross-env": "7.0.3",
"css-loader": "4.0.0",
"d3-axis": "1.0.x",
"d3-scale": "1.0.x",
"d3-selection": "1.3.x",
"eslint": "8.13.0",
"eslint": "8.11.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.8.0",
"eslint-plugin-vue": "8.5.0",
@@ -37,7 +38,7 @@
"imports-loader": "0.8.0",
"jasmine-core": "4.0.1",
"jsdoc": "3.5.5",
"karma": "6.3.18",
"karma": "6.3.15",
"karma-chrome-launcher": "3.1.1",
"karma-cli": "2.0.0",
"karma-coverage": "2.1.1",
@@ -46,14 +47,14 @@
"karma-jasmine": "4.0.1",
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.34",
"karma-spec-reporter": "0.0.33",
"karma-webpack": "5.0.0",
"lighthouse": "9.5.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.6.0",
"moment": "2.29.1",
"moment-duration-format": "2.3.2",
"moment-duration-format": "2.2.2",
"moment-timezone": "0.5.34",
"node-bourbon": "4.2.3",
"painterro": "1.2.56",
@@ -61,14 +62,14 @@
"plotly.js-gl2d-dist": "2.5.0",
"printj": "1.3.1",
"request": "2.88.2",
"resolve-url-loader": "5.0.0",
"resolve-url-loader": "4.0.0",
"sass": "1.49.9",
"sass-loader": "12.6.0",
"sinon": "13.0.1",
"style-loader": "^1.0.1",
"uuid": "3.3.3",
"vue": "2.6.14",
"vue-eslint-parser": "8.3.0",
"vue-eslint-parser": "8.2.0",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.6.14",
"webpack": "5.68.0",
@@ -112,9 +113,6 @@
"engines": {
"node": ">=14.19.1"
},
"overrides": {
"core-js": "3.21.1"
},
"browserslist": [
"Firefox ESR",
"not IE 11",

View File

@@ -241,6 +241,7 @@ define([
this.branding = BrandingAPI.default;
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.Chart());
this.install(this.plugins.TelemetryTable.default());

View File

@@ -1,6 +1,5 @@
import AutoCompleteField from './components/controls/AutoCompleteField.vue';
import ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue';
import CheckBoxField from './components/controls/CheckBoxField.vue';
import Datetime from './components/controls/Datetime.vue';
import FileInput from './components/controls/FileInput.vue';
import Locator from './components/controls/Locator.vue';
@@ -8,13 +7,11 @@ import NumberField from './components/controls/NumberField.vue';
import SelectField from './components/controls/SelectField.vue';
import TextAreaField from './components/controls/TextAreaField.vue';
import TextField from './components/controls/TextField.vue';
import ToggleSwitchField from './components/controls/ToggleSwitchField.vue';
import Vue from 'vue';
export const DEFAULT_CONTROLS_MAP = {
'autocomplete': AutoCompleteField,
'checkbox': CheckBoxField,
'composite': ClockDisplayFormatField,
'datetime': Datetime,
'file-input': FileInput,
@@ -22,8 +19,7 @@ export const DEFAULT_CONTROLS_MAP = {
'numberfield': NumberField,
'select': SelectField,
'textarea': TextAreaField,
'textfield': TextField,
'toggleSwitch': ToggleSwitchField
'textfield': TextField
};
export default class FormControl {
@@ -98,3 +94,4 @@ export default class FormControl {
};
}
}

View File

@@ -79,11 +79,9 @@ export default {
rowClass() {
let cssClass = this.cssClass;
if (!this.row.required) {
return;
}
if (this.row.required) {
cssClass = `${cssClass} req`;
}
if (this.visited && this.valid !== undefined) {
if (this.valid === true) {

View File

@@ -1,55 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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>
<span class="form-control shell">
<span
class="field control"
:class="model.cssClass"
>
<input
type="checkbox"
:checked="isChecked"
@input="toggleCheckBox"
>
</span>
</span>
</template>
<script>
import toggleMixin from '../../toggle-check-box-mixin';
export default {
mixins: [toggleMixin],
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
isChecked: this.model.value
};
}
};
</script>

View File

@@ -58,6 +58,7 @@ export default {
},
methods: {
updateText() {
console.log('updateText', this.field);
const data = {
model: this.model,
value: this.field

View File

@@ -1,62 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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>
<span class="form-control shell">
<span
class="field control"
:class="model.cssClass"
>
<ToggleSwitch
id="switchId"
:checked="isChecked"
@change="toggleCheckBox"
/>
</span>
</span>
</template>
<script>
import toggleMixin from '../../toggle-check-box-mixin';
import ToggleSwitch from '@/ui/components/ToggleSwitch.vue';
import uuid from 'uuid';
export default {
components: {
ToggleSwitch
},
mixins: [toggleMixin],
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
switchId: `toggleSwitch-${uuid}`,
isChecked: this.model.value
};
}
};
</script>

View File

@@ -1,19 +0,0 @@
export default {
data() {
return {
isChecked: false
};
},
methods: {
toggleCheckBox(event) {
this.isChecked = !this.isChecked;
const data = {
model: this.model,
value: this.isChecked
};
this.$emit('onChange', data);
}
}
};

View File

@@ -43,7 +43,6 @@ class InMemorySearchProvider {
this.openmct = openmct;
this.indexedIds = {};
this.indexedCompositions = {};
this.idsToIndex = [];
this.pendingIndex = {};
this.pendingRequests = 0;
@@ -59,6 +58,7 @@ class InMemorySearchProvider {
this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
this.onerror = this.onWorkerError.bind(this);
this.startIndexing = this.startIndexing.bind(this);
this.onMutationOfIndexedObject = this.onMutationOfIndexedObject.bind(this);
this.openmct.on('start', this.startIndexing);
this.openmct.on('destroy', () => {
@@ -68,9 +68,6 @@ class InMemorySearchProvider {
this.worker.port.onmessageerror = null;
this.worker.port.close();
}
this.destroyObservers(this.indexedIds);
this.destroyObservers(this.indexedCompositions);
});
}
@@ -140,7 +137,7 @@ class InMemorySearchProvider {
};
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
const domainObject = await this.openmct.objects.get(identifier);
const domainObject = await this.openmct.objects.get(identifier.key);
return domainObject;
}));
@@ -216,52 +213,29 @@ class InMemorySearchProvider {
}
}
onNameMutation(domainObject, name) {
onMutationOfIndexedObject(domainObject) {
const provider = this;
domainObject.name = name;
provider.index(domainObject);
}
onCompositionMutation(domainObject, composition) {
const provider = this;
const indexedComposition = domainObject.composition;
const identifiersToIndex = composition
.filter(identifier => !indexedComposition
.some(indexedIdentifier => this.openmct.objects
.areIdsEqual([identifier, indexedIdentifier])));
identifiersToIndex.forEach(identifier => {
this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex));
});
provider.index(domainObject.identifier, domainObject);
}
/**
* Pass a domainObject to the worker to be indexed.
* If the object has composition, schedule those ids for later indexing.
* Watch for object changes and re-index object and children if so
* Pass an id and model to the worker to be indexed. If the model has
* composition, schedule those ids for later indexing.
*
* @private
* @param domainObject a domainObject
* @param id a model id
* @param model a model
*/
async index(domainObject) {
async index(id, domainObject) {
const provider = this;
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
const keyString = this.openmct.objects.makeKeyString(id);
if (!this.indexedIds[keyString]) {
this.indexedIds[keyString] = this.openmct.objects.observe(
domainObject,
'name',
this.onNameMutation.bind(this, domainObject)
);
this.indexedCompositions[keyString] = this.openmct.objects.observe(
domainObject,
'composition',
this.onCompositionMutation.bind(this, domainObject)
);
this.openmct.objects.observe(domainObject, `*`, this.onMutationOfIndexedObject);
}
if ((keyString !== 'ROOT')) {
this.indexedIds[keyString] = true;
if ((id.key !== 'ROOT')) {
if (this.worker) {
this.worker.port.postMessage({
request: 'index',
@@ -273,12 +247,15 @@ class InMemorySearchProvider {
}
}
const composition = this.openmct.composition.get(domainObject);
const composition = this.openmct.composition.registry.find(foundComposition => {
return foundComposition.appliesTo(domainObject);
});
if (composition !== undefined) {
const children = await composition.load();
children.forEach(child => provider.scheduleForIndexing(child.identifier));
if (composition) {
const childIdentifiers = await composition.load(domainObject);
childIdentifiers.forEach(function (childIdentifier) {
provider.scheduleForIndexing(childIdentifier);
});
}
}
@@ -294,12 +271,12 @@ class InMemorySearchProvider {
const provider = this;
this.pendingRequests += 1;
const domainObject = await this.openmct.objects.get(keyString);
const identifier = await this.openmct.objects.parseKeyString(keyString);
const domainObject = await this.openmct.objects.get(identifier.key);
delete provider.pendingIndex[keyString];
try {
if (domainObject) {
await provider.index(domainObject);
await provider.index(identifier, domainObject);
}
} catch (error) {
console.warn('Failed to index domain object ' + keyString, error);
@@ -370,16 +347,6 @@ class InMemorySearchProvider {
};
this.onWorkerMessage(eventToReturn);
}
destroyObservers(observers) {
Object.entries(observers).forEach(([keyString, unobserve]) => {
if (typeof unobserve === 'function') {
unobserve();
}
delete observers[keyString];
});
}
}
export default InMemorySearchProvider;

View File

@@ -105,18 +105,13 @@ describe("The Object API Search Function", () => {
beforeEach((done) => {
openmct = createOpenMct();
const defaultObjectProvider = openmct.objects.getProvider({
key: '',
namespace: ''
});
openmct.objects.addProvider('foo', defaultObjectProvider);
spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough();
spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough();
openmct.on('start', async () => {
mockIdentifier1 = {
key: 'some-object',
namespace: 'foo'
namespace: 'some-namespace'
};
mockDomainObject1 = {
type: 'clock',
@@ -125,7 +120,7 @@ describe("The Object API Search Function", () => {
};
mockIdentifier2 = {
key: 'some-other-object',
namespace: 'foo'
namespace: 'some-namespace'
};
mockDomainObject2 = {
type: 'clock',
@@ -134,16 +129,16 @@ describe("The Object API Search Function", () => {
};
mockIdentifier3 = {
key: 'yet-another-object',
namespace: 'foo'
namespace: 'some-namespace'
};
mockDomainObject3 = {
type: 'clock',
name: 'redBear',
identifier: mockIdentifier3
};
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
done();
});
openmct.startHeadless();
@@ -180,9 +175,9 @@ describe("The Object API Search Function", () => {
beforeEach(async () => {
openmct.objects.inMemorySearchProvider.worker = null;
// reindex locally
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
});
it("calls local search", () => {
openmct.objects.search('foo');

View File

@@ -22,14 +22,12 @@
export default class Transaction {
constructor(objectAPI) {
this.dirtyObjects = {};
this.dirtyObjects = new Set();
this.objectAPI = objectAPI;
}
add(object) {
const key = this.objectAPI.makeKeyString(object.identifier);
this.dirtyObjects[key] = object;
this.dirtyObjects.add(object);
}
cancel() {
@@ -39,8 +37,7 @@ export default class Transaction {
commit() {
const promiseArray = [];
const save = this.objectAPI.save.bind(this.objectAPI);
Object.values(this.dirtyObjects).forEach(object => {
this.dirtyObjects.forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, save));
});
@@ -51,9 +48,7 @@ export default class Transaction {
return new Promise((resolve, reject) => {
action(object)
.then((success) => {
const key = this.objectAPI.makeKeyString(object.identifier);
delete this.dirtyObjects[key];
this.dirtyObjects.delete(object);
resolve(success);
})
.catch(reject);
@@ -62,8 +57,7 @@ export default class Transaction {
getDirtyObject(identifier) {
let dirtyObject;
Object.values(this.dirtyObjects).forEach(object => {
this.dirtyObjects.forEach(object => {
const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier);
if (areIdsEqual) {
dirtyObject = object;
@@ -73,11 +67,14 @@ export default class Transaction {
return dirtyObject;
}
start() {
this.dirtyObjects = new Set();
}
_clear() {
const promiseArray = [];
const refresh = this.objectAPI.refresh.bind(this.objectAPI);
Object.values(this.dirtyObjects).forEach(object => {
this.dirtyObjects.forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, refresh));
});

View File

@@ -34,24 +34,24 @@ describe("Transaction Class", () => {
});
it('has no dirty objects', () => {
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
expect(transaction.dirtyObjects.size).toEqual(0);
});
it('add(), adds object to dirtyObjects', () => {
const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
expect(transaction.dirtyObjects.size).toEqual(1);
});
it('cancel(), clears all dirtyObjects', (done) => {
const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
expect(transaction.dirtyObjects.size).toEqual(3);
transaction.cancel()
.then(success => {
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
expect(transaction.dirtyObjects.size).toEqual(0);
}).finally(done);
});
@@ -59,12 +59,12 @@ describe("Transaction Class", () => {
const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
expect(transaction.dirtyObjects.size).toEqual(3);
spyOn(objectAPI, 'save').and.callThrough();
transaction.commit()
.then(success => {
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
expect(transaction.dirtyObjects.size).toEqual(0);
expect(objectAPI.save.calls.count()).toEqual(3);
}).finally(done);
});
@@ -73,7 +73,7 @@ describe("Transaction Class", () => {
const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
expect(transaction.dirtyObjects.size).toEqual(1);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(dirtyObject).toEqual(mockDomainObjects[0]);
@@ -82,7 +82,7 @@ describe("Transaction Class", () => {
it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => {
const mockDomainObjects = createMockDomainObjects();
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
expect(transaction.dirtyObjects.size).toEqual(0);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(dirtyObject).toEqual(undefined);

View File

@@ -172,7 +172,6 @@ export class TelemetryCollection extends EventEmitter {
* @private
*/
_processNewTelemetry(telemetryData) {
performance.mark('tlm:process:start');
if (telemetryData === undefined) {
return;
}
@@ -353,7 +352,6 @@ export class TelemetryCollection extends EventEmitter {
* @todo handle subscriptions more granually
*/
_reset() {
performance.mark('tlm:reset');
this.boundedTelemetry = [];
this.futureBuffer = [];

View File

@@ -1,3 +1,4 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
@@ -113,12 +114,14 @@ export default {
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.bounds = this.openmct.time.bounds();
this.limitEvaluator = this.openmct
.telemetry
.limitEvaluator(this.domainObject);
this.openmct.time.on('timeSystem', this.updateTimeSystem);
this.openmct.time.on('bounds', this.updateBounds);
this.timestampKey = this.openmct.time.timeSystem().key;
@@ -132,41 +135,72 @@ export default {
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
size: 1,
strategy: 'latest'
});
this.telemetryCollection.on('add', this.setLatestValues);
this.telemetryCollection.on('clear', this.resetValues);
this.telemetryCollection.load();
this.unsubscribe = this.openmct
.telemetry
.subscribe(this.domainObject, this.setLatestValues);
this.requestHistory();
if (this.hasUnits) {
this.setUnit();
}
},
destroyed() {
this.unsubscribe();
this.openmct.time.off('timeSystem', this.updateTimeSystem);
this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.resetValues);
this.telemetryCollection.destroy();
this.openmct.time.off('bounds', this.updateBounds);
},
methods: {
updateView() {
if (!this.updatingView) {
this.updatingView = true;
requestAnimationFrame(() => {
this.timestamp = this.getParsedTimestamp(this.latestDatum);
let newTimestamp = this.getParsedTimestamp(this.latestDatum);
if (this.shouldUpdate(newTimestamp)) {
this.timestamp = newTimestamp;
this.datum = this.latestDatum;
}
this.updatingView = false;
});
}
},
setLatestValues(data) {
this.latestDatum = data[data.length - 1];
setLatestValues(datum) {
this.latestDatum = datum;
this.updateView();
},
shouldUpdate(newTimestamp) {
return this.inBounds(newTimestamp)
&& (this.timestamp === undefined || newTimestamp > this.timestamp);
},
requestHistory() {
this.openmct
.telemetry
.request(this.domainObject, {
start: this.bounds.start,
end: this.bounds.end,
size: 1,
strategy: 'latest'
})
.then((array) => this.setLatestValues(array[array.length - 1]))
.catch((error) => {
console.warn('Error fetching data', error);
});
},
updateBounds(bounds, isTick) {
this.bounds = bounds;
if (!isTick) {
this.resetValues();
this.requestHistory();
}
},
inBounds(timestamp) {
return timestamp >= this.bounds.start && timestamp <= this.bounds.end;
},
updateTimeSystem(timeSystem) {
this.resetValues();
this.timestampKey = timeSystem.key;
},
updateViewContext() {
@@ -207,3 +241,4 @@ export default {
}
};
</script>

View File

@@ -46,7 +46,6 @@ describe("The LAD Table", () => {
let openmct;
let ladPlugin;
let historicalProvider;
let parent;
let child;
let telemetryCount = 3;
@@ -82,13 +81,6 @@ describe("The LAD Table", () => {
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
historicalProvider = {
request: () => {
return Promise.resolve([]);
}
};
spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider);
openmct.time.bounds({
start: bounds.start,
end: bounds.end
@@ -155,7 +147,7 @@ describe("The LAD Table", () => {
// add another telemetry object as composition in lad table to test multi rows
mockObj.ladTable.composition.push(anotherTelemetryObj.identifier);
beforeEach(async (done) => {
beforeEach(async () => {
let telemetryRequestResolve;
let telemetryObjectResolve;
let anotherTelemetryObjectResolve;
@@ -174,12 +166,11 @@ describe("The LAD Table", () => {
callBack();
});
historicalProvider.request = () => {
openmct.telemetry.request.and.callFake(() => {
telemetryRequestResolve(mockTelemetry);
return telemetryRequestPromise;
};
});
openmct.objects.get.and.callFake((obj) => {
if (obj.key === 'telemetry-object') {
telemetryObjectResolve(mockObj.telemetry);
@@ -204,8 +195,6 @@ describe("The LAD Table", () => {
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
await Vue.nextTick();
done();
});
it("should show one row per object in the composition", () => {

View File

@@ -27,7 +27,7 @@
:href="url"
>
<div class="c-condition-widget__label">
{{ label }}
{{ internalDomainObject.conditionalLabel || internalDomainObject.label }}
</div>
</component>
</template>
@@ -39,112 +39,28 @@ export default {
inject: ['openmct', 'domainObject'],
data: function () {
return {
conditionalLabel: '',
conditionSetIdentifier: null,
domainObjectLabel: '',
url: null,
urlDefined: false,
useConditionSetOutputAsLabel: false
internalDomainObject: this.domainObject
};
},
computed: {
label() {
return this.useConditionSetOutputAsLabel
? this.conditionalLabel
: this.domainObjectLabel
;
}
urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
},
watch: {
conditionSetIdentifier: {
handler(newValue, oldValue) {
if (!oldValue || !newValue || !this.openmct.objects.areIdsEqual(newValue, oldValue)) {
return;
}
this.listenToConditionSetChanges();
},
deep: true
url() {
return this.urlDefined ? sanitizeUrl(this.internalDomainObject.url) : null;
}
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.updateDomainObject);
if (this.domainObject) {
this.updateDomainObject(this.domainObject);
this.listenToConditionSetChanges();
}
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
},
beforeDestroy() {
this.conditionSetIdentifier = null;
if (this.unlisten) {
this.unlisten();
}
this.stopListeningToConditionSetChanges();
},
methods: {
async listenToConditionSetChanges() {
if (!this.conditionSetIdentifier) {
return;
}
const conditionSetDomainObject = await this.openmct.objects.get(this.conditionSetIdentifier);
this.stopListeningToConditionSetChanges();
if (!conditionSetDomainObject) {
this.openmct.notifications.alert('Unable to find condition set');
}
this.telemetryCollection = this.openmct.telemetry.requestCollection(conditionSetDomainObject, {
size: 1,
strategy: 'latest'
});
this.telemetryCollection.on('add', this.updateConditionLabel, this);
this.telemetryCollection.load();
},
stopListeningToConditionSetChanges() {
if (this.telemetryCollection) {
this.telemetryCollection.off('add', this.updateConditionLabel, this);
this.telemetryCollection.destroy();
this.telemetryCollection = null;
}
},
updateConditionLabel([latestDatum]) {
if (!this.conditionSetIdentifier) {
this.stopListeningToConditionSetChanges();
return;
}
this.conditionalLabel = latestDatum.output || '';
},
updateDomainObject(domainObject) {
if (this.domainObjectLabel !== domainObject.label) {
this.domainObjectLabel = domainObject.label;
}
const urlDefined = domainObject.url && domainObject.url.length > 0;
if (this.urlDefined !== urlDefined) {
this.urlDefined = urlDefined;
}
const url = this.urlDefined ? sanitizeUrl(domainObject.url) : null;
if (this.url !== url) {
this.url = url;
}
const conditionSetIdentifier = domainObject.configuration.objectStyles.conditionSetIdentifier;
if (this.conditionSetIdentifier !== conditionSetIdentifier) {
this.conditionSetIdentifier = conditionSetIdentifier;
}
const useConditionSetOutputAsLabel = this.conditionSetIdentifier && domainObject.configuration.useConditionSetOutputAsLabel;
if (this.useConditionSetOutputAsLabel !== useConditionSetOutputAsLabel) {
this.useConditionSetOutputAsLabel = useConditionSetOutputAsLabel;
}
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
}
}
};

View File

@@ -222,20 +222,20 @@ export default {
.then(this.setObject);
}
this.openmct.time.on("bounds", this.refreshData);
this.status = this.openmct.status.get(this.item.identifier);
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
},
beforeDestroy() {
this.removeSubscription();
this.removeStatusListener();
if (this.removeSelectable) {
this.removeSelectable();
}
this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.refreshData);
this.telemetryCollection.destroy();
this.openmct.time.off("bounds", this.refreshData);
if (this.mutablePromise) {
this.mutablePromise.then(() => {
@@ -253,9 +253,34 @@ export default {
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue}${unit}`;
},
setLatestValues(data) {
requestHistoricalData() {
let bounds = this.openmct.time.bounds();
let options = {
start: bounds.start,
end: bounds.end,
size: 1,
strategy: 'latest'
};
this.openmct.telemetry.request(this.domainObject, options)
.then(data => {
if (data.length > 0) {
this.latestDatum = data[data.length - 1];
this.updateView();
}
});
},
subscribeToObject() {
this.subscription = this.openmct.telemetry.subscribe(this.domainObject, function (datum) {
const key = this.openmct.time.timeSystem().key;
const datumTimeStamp = datum[key];
if (this.openmct.time.clock() !== undefined
|| (datumTimeStamp
&& (this.openmct.time.bounds().end >= datumTimeStamp))
) {
this.latestDatum = datum;
this.updateView();
}
}.bind(this));
},
updateView() {
if (!this.updatingView) {
@@ -266,10 +291,17 @@ export default {
});
}
},
removeSubscription() {
if (this.subscription) {
this.subscription();
this.subscription = undefined;
}
},
refreshData(bounds, isTick) {
if (!isTick) {
this.latestDatum = undefined;
this.updateView();
this.requestHistoricalData(this.domainObject);
}
},
setObject(domainObject) {
@@ -283,13 +315,8 @@ export default {
const valueMetadata = this.metadata.value(this.item.value);
this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format);
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
size: 1,
strategy: 'latest'
});
this.telemetryCollection.on('add', this.setLatestValues);
this.telemetryCollection.on('clear', this.refreshData);
this.telemetryCollection.load();
this.requestHistoricalData();
this.subscribeToObject();
this.currentObjectPath = this.objectPath.slice();
this.currentObjectPath.unshift(this.domainObject);

View File

@@ -57,7 +57,7 @@
/>
<drop-hint
:key="'hint-' + i"
:key="i"
class="c-fl-frame__drop-hint"
:index="i"
:allow-drop="allowDrop"
@@ -66,7 +66,7 @@
<resize-handle
v-if="(i !== frames.length - 1)"
:key="'handle-' + i"
:key="i"
:index="i"
:orientation="rowsLayout ? 'horizontal' : 'vertical'"
:is-editing="isEditing"

View File

@@ -1,5 +1,5 @@
<template>
<div class="c-table c-table--sortable c-list-view c-list-view--sticky-header c-list-view--selectable">
<div class="c-table c-table--sortable c-list-view">
<table class="c-table__body">
<thead class="c-table__header">
<tr>

View File

@@ -0,0 +1,32 @@
/******************************* LIST VIEW */
.c-list-view {
overflow-x: auto !important;
overflow-y: auto;
tbody tr {
background: $colorListItemBg;
transition: $transOut;
}
body.desktop & {
tbody tr {
cursor: pointer;
&:hover {
background: $colorListItemBgHov;
filter: $filterHov;
transition: $transIn;
}
}
}
td {
$p: floor($interiorMargin * 1.5);
@include ellipsize();
line-height: 120%; // Needed for icon alignment
max-width: 0;
padding-top: $p;
padding-bottom: $p;
width: 25%;
}
}

View File

@@ -90,9 +90,6 @@ export default class CreateWizard {
rows: this.properties.map(property => {
const row = JSON.parse(JSON.stringify(property));
row.value = this.getValue(row);
if (property.validate) {
row.validate = property.validate;
}
return row;
}).filter(row => row && row.control)

View File

@@ -51,8 +51,7 @@ export default class EditPropertiesAction extends PropertiesAction {
/**
* @private
*/
_onSave(changes) {
try {
async _onSave(changes) {
Object.entries(changes).forEach(([key, value]) => {
const properties = key.split('.');
let object = this.domainObject;
@@ -67,13 +66,26 @@ export default class EditPropertiesAction extends PropertiesAction {
});
object = value;
this.openmct.objects.mutate(this.domainObject, key, value);
this.openmct.notifications.info('Save successful');
});
} catch (error) {
this.domainObject.modified = Date.now();
// Show saving progress dialog
let dialog = this.openmct.overlays.progressDialog({
progressPerc: 'unknown',
message: 'Do not navigate away from this page or close this browser tab while this message is displayed.',
iconClass: 'info',
title: 'Saving'
});
const success = await this.openmct.objects.save(this.domainObject);
if (success) {
this.openmct.notifications.info('Save successful');
} else {
this.openmct.notifications.error('Error saving objects');
console.error(error);
}
dialog.dismiss();
}
/**

View File

@@ -1,199 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 GaugeViewProvider from './GaugeViewProvider';
import GaugeFormController from './components/GaugeFormController.vue';
import Vue from 'vue';
export const GAUGE_TYPES = [
['Filled Dial', 'dial-filled'],
['Needle Dial', 'dial-needle'],
['Vertical Meter', 'meter-vertical'],
['Vertical Meter Inverted', 'meter-vertical-inverted'],
['Horizontal Meter', 'meter-horizontal']
];
export default function () {
return function install(openmct) {
openmct.objectViews.addProvider(new GaugeViewProvider(openmct));
openmct.forms.addNewFormControl('gauge-controller', getGaugeFormController(openmct));
openmct.types.addType('gauge', {
name: "Gauge",
creatable: true,
description: "Graphically visualize a telemetry element's current value between a minimum and maximum.",
cssClass: 'icon-gauge',
initialize(domainObject) {
domainObject.composition = [];
domainObject.configuration = {
gaugeController: {
gaugeType: GAUGE_TYPES[0][1],
isDisplayMinMax: true,
isDisplayCurVal: true,
isUseTelemetryLimits: true,
limitLow: 10,
limitHigh: 90,
max: 100,
min: 0,
precision: 2
}
};
},
form: [
{
name: "Display current value",
control: "toggleSwitch",
cssClass: "l-input",
key: "isDisplayCurVal",
property: [
"configuration",
"gaugeController",
"isDisplayCurVal"
]
},
{
name: "Display range values",
control: "toggleSwitch",
cssClass: "l-input",
key: "isDisplayMinMax",
property: [
"configuration",
"gaugeController",
"isDisplayMinMax"
]
},
{
name: "Float precision",
control: "numberfield",
cssClass: "l-input-sm",
key: "precision",
property: [
"configuration",
"gaugeController",
"precision"
]
},
{
name: "Gauge type",
options: GAUGE_TYPES.map(type => {
return {
name: type[0],
value: type[1]
};
}),
control: "select",
cssClass: "l-input-sm",
key: "gaugeController",
property: [
"configuration",
"gaugeController",
"gaugeType"
]
},
{
name: "Value ranges and limits",
control: "gauge-controller",
cssClass: "l-input",
key: "gaugeController",
required: false,
hideFromInspector: true,
property: [
"configuration",
"gaugeController"
],
validate: ({ value }, callback) => {
if (value.isUseTelemetryLimits) {
return true;
}
const { min, max, limitLow, limitHigh } = value;
const valid = {
min: true,
max: true,
limitLow: true,
limitHigh: true
};
if (min === '') {
valid.min = false;
}
if (max === '') {
valid.max = false;
}
if (max < min) {
valid.min = false;
valid.max = false;
}
if (limitLow !== '') {
valid.limitLow = min <= limitLow && limitLow < max;
}
if (limitHigh !== '') {
valid.limitHigh = min < limitHigh && limitHigh <= max;
}
if (valid.limitLow && valid.limitHigh
&& limitLow !== '' && limitHigh !== ''
&& limitLow > limitHigh) {
valid.limitLow = false;
valid.limitHigh = false;
}
if (callback) {
callback(valid);
}
return valid.min && valid.max && valid.limitLow && valid.limitHigh;
}
}
]
});
};
function getGaugeFormController(openmct) {
return {
show(element, model, onChange) {
const rowComponent = new Vue({
el: element,
components: {
GaugeFormController
},
provide: {
openmct
},
data() {
return {
model,
onChange
};
},
template: `<GaugeFormController :model="model" @onChange="onChange"></GaugeFormController>`
});
return rowComponent;
}
};
}
}

View File

@@ -1,801 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 { debounce } from 'lodash';
import Vue from 'vue';
let gaugeDomainObject = {
identifier: {
key: 'gauge',
namespace: 'test-namespace'
},
type: 'gauge',
composition: []
};
describe('Gauge plugin', () => {
let openmct;
let child;
let gaugeHolder;
beforeEach((done) => {
gaugeHolder = document.createElement('div');
gaugeHolder.style.display = 'block';
gaugeHolder.style.width = '1920px';
gaugeHolder.style.height = '1080px';
child = document.createElement('div');
gaugeHolder.appendChild(child);
openmct = createOpenMct();
openmct.on('start', done);
openmct.install(openmct.plugins.Gauge());
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('Plugin installed by default', () => {
const gaugueType = openmct.types.get('gauge');
expect(gaugueType).not.toBeNull();
expect(gaugueType.definition.name).toEqual('Gauge');
});
it('Gaugue plugin is creatable', () => {
const gaugueType = openmct.types.get('gauge');
expect(gaugueType.definition.creatable).toBeTrue();
});
it('Gaugue plugin is creatable', () => {
const gaugueType = openmct.types.get('gauge');
expect(gaugueType.definition.creatable).toBeTrue();
});
it('Gaugue form controller', () => {
const gaugeController = openmct.forms.getFormControl('gauge-controller');
expect(gaugeController).toBeDefined();
});
describe('Gaugue with Filled Dial', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
let mutablegaugeObject;
let randomValue;
const minValue = -1;
const maxValue = 1;
beforeEach(() => {
randomValue = Math.random();
gaugeViewObject = {
...gaugeDomainObject,
configuration: {
gaugeController: {
gaugeType: 'dial-filled',
isDisplayMinMax: true,
isDisplayCurVal: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
max: maxValue,
min: minValue,
precision: 2
}
},
composition: [
{
namespace: 'test-namespace',
key: 'test-object'
}
],
id: 'test-object',
name: 'gauge'
};
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
'get',
'create',
'update',
'observe'
]);
openmct.editor = {};
openmct.editor.isEditing = () => false;
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {});
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
spyOn(openmct.telemetry, 'getMetadata').and.returnValue({
valuesForHints: () => {
return [
{
source: 'sin'
}
];
},
value: () => 1
});
spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({
parse: () => {
return 2000;
}
});
spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({
sin: {
format: (datum) => {
return randomValue;
}
}
});
spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
start: 1000,
end: 5000
});
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
gaugeView.show(child);
return Vue.nextTick();
});
});
afterEach(() => {
gaugeView.destroy();
return resetApplicationState(openmct);
});
it('provides gauge view', () => {
expect(gaugeViewProvider).toBeDefined();
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true);
});
it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${minValue} ${maxValue}`);
});
it('renders correct current value', (done) => {
function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.js-dial-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done();
}
const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);
Vue.nextTick(debouncedWatchUpdate);
});
});
describe('Gaugue with Needle Dial', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
let mutablegaugeObject;
let randomValue;
const minValue = -1;
const maxValue = 1;
beforeEach(() => {
randomValue = Math.random();
gaugeViewObject = {
...gaugeDomainObject,
configuration: {
gaugeController: {
gaugeType: 'dial-needle',
isDisplayMinMax: true,
isDisplayCurVal: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
max: maxValue,
min: minValue,
precision: 2
}
},
composition: [
{
namespace: 'test-namespace',
key: 'test-object'
}
],
id: 'test-object',
name: 'gauge'
};
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
'get',
'create',
'update',
'observe'
]);
openmct.editor = {};
openmct.editor.isEditing = () => false;
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {});
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
spyOn(openmct.telemetry, 'getMetadata').and.returnValue({
valuesForHints: () => {
return [
{
source: 'sin'
}
];
},
value: () => 1
});
spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({
parse: () => {
return 2000;
}
});
spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({
sin: {
format: (datum) => {
return randomValue;
}
}
});
spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
start: 1000,
end: 5000
});
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
gaugeView.show(child);
return Vue.nextTick();
});
});
afterEach(() => {
gaugeView.destroy();
return resetApplicationState(openmct);
});
it('provides gauge view', () => {
expect(gaugeViewProvider).toBeDefined();
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true);
});
it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${minValue} ${maxValue}`);
});
it('renders correct current value', (done) => {
function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.js-dial-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done();
}
const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);
Vue.nextTick(debouncedWatchUpdate);
});
});
describe('Gaugue with Vertical Meter', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
let mutablegaugeObject;
let randomValue;
const minValue = -1;
const maxValue = 1;
beforeEach(() => {
randomValue = Math.random();
gaugeViewObject = {
...gaugeDomainObject,
configuration: {
gaugeController: {
gaugeType: 'meter-vertical',
isDisplayMinMax: true,
isDisplayCurVal: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
max: maxValue,
min: minValue,
precision: 2
}
},
composition: [
{
namespace: 'test-namespace',
key: 'test-object'
}
],
id: 'test-object',
name: 'gauge'
};
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
'get',
'create',
'update',
'observe'
]);
openmct.editor = {};
openmct.editor.isEditing = () => false;
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {});
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
spyOn(openmct.telemetry, 'getMetadata').and.returnValue({
valuesForHints: () => {
return [
{
source: 'sin'
}
];
},
value: () => 1
});
spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({
parse: () => {
return 2000;
}
});
spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({
sin: {
format: (datum) => {
return randomValue;
}
}
});
spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
start: 1000,
end: 5000
});
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
gaugeView.show(child);
return Vue.nextTick();
});
});
afterEach(() => {
gaugeView.destroy();
return resetApplicationState(openmct);
});
it('provides gauge view', () => {
expect(gaugeViewProvider).toBeDefined();
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true);
});
it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.js-gauge-meter-range').textContent).toEqual(`${maxValue} ${minValue}`);
});
it('renders correct current value', (done) => {
function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.js-meter-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done();
}
const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);
Vue.nextTick(debouncedWatchUpdate);
});
});
describe('Gaugue with Vertical Meter Inverted', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
let mutablegaugeObject;
beforeEach(() => {
gaugeViewObject = {
...gaugeDomainObject,
configuration: {
gaugeController: {
gaugeType: 'meter-vertical',
isDisplayMinMax: true,
isDisplayCurVal: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
max: 1,
min: -1,
precision: 2
}
},
id: 'test-object',
name: 'gauge'
};
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
'get',
'create',
'update',
'observe'
]);
openmct.editor = {};
openmct.editor.isEditing = () => false;
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {});
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
gaugeView.show(child);
return Vue.nextTick();
});
});
afterEach(() => {
gaugeView.destroy();
return resetApplicationState(openmct);
});
it('provides gauge view', () => {
expect(gaugeViewProvider).toBeDefined();
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true);
});
});
describe('Gaugue with Horizontal Meter', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
let mutablegaugeObject;
beforeEach(() => {
gaugeViewObject = {
...gaugeDomainObject,
configuration: {
gaugeController: {
gaugeType: 'meter-vertical',
isDisplayMinMax: true,
isDisplayCurVal: true,
isUseTelemetryLimits: false,
limitLow: -0.9,
limitHigh: 0.9,
max: 1,
min: -1,
precision: 2
}
},
id: 'test-object',
name: 'gauge'
};
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
'get',
'create',
'update',
'observe'
]);
openmct.editor = {};
openmct.editor.isEditing = () => false;
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {});
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
gaugeView.show(child);
return Vue.nextTick();
});
});
afterEach(() => {
gaugeView.destroy();
return resetApplicationState(openmct);
});
it('provides gauge view', () => {
expect(gaugeViewProvider).toBeDefined();
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
const curveElement = gaugeHolder.querySelector('.c-meter');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement);
expect(hasMajorElements).toBe(true);
});
});
describe('Gaugue with Filled Dial with Use Telemetry Limits', () => {
let gaugeViewProvider;
let gaugeView;
let gaugeViewObject;
let mutablegaugeObject;
let randomValue;
beforeEach(() => {
randomValue = Math.random();
gaugeViewObject = {
...gaugeDomainObject,
configuration: {
gaugeController: {
gaugeType: 'dial-filled',
isDisplayMinMax: true,
isDisplayCurVal: true,
isUseTelemetryLimits: true,
limitLow: 10,
limitHigh: 90,
max: 100,
min: 0,
precision: 2
}
},
composition: [
{
namespace: 'test-namespace',
key: 'test-object'
}
],
id: 'test-object',
name: 'gauge'
};
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
'get',
'create',
'update',
'observe'
]);
openmct.editor = {};
openmct.editor.isEditing = () => false;
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {});
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
spyOn(openmct.telemetry, 'getMetadata').and.returnValue({
valuesForHints: () => {
return [
{
source: 'sin'
}
];
},
value: () => 1
});
spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({
parse: () => {
return 2000;
}
});
spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({
sin: {
format: (datum) => {
return randomValue;
}
}
});
spyOn(openmct.telemetry, 'getLimits').and.returnValue(
{
limits: () => Promise.resolve({
CRITICAL: {
high: 0.99,
low: -0.99
}
})
}
);
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
spyOn(openmct.time, 'bounds').and.returnValue({
start: 1000,
end: 5000
});
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
gaugeView.show(child);
return Vue.nextTick();
});
});
afterEach(() => {
gaugeView.destroy();
return resetApplicationState(openmct);
});
it('provides gauge view', () => {
expect(gaugeViewProvider).toBeDefined();
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true);
});
it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${gaugeViewObject.configuration.gaugeController.min} ${gaugeViewObject.configuration.gaugeController.max}`);
});
it('renders correct current value', (done) => {
function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.js-dial-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done();
}
const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);
Vue.nextTick(debouncedWatchUpdate);
});
});
});

View File

@@ -1,67 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 GaugeComponent from './components/Gauge.vue';
import Vue from 'vue';
export default function GaugeViewProvider(openmct) {
return {
key: 'gauge',
name: 'Gauge',
cssClass: 'icon-gauge',
canView: function (domainObject) {
return domainObject.type === 'gauge';
},
canEdit: function (domainObject) {
if (domainObject.type === 'gauge') {
return true;
}
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
GaugeComponent
},
provide: {
openmct,
domainObject,
composition: openmct.composition.get(domainObject)
},
template: '<gauge-component></gauge-component>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
}

View File

@@ -1,566 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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="c-gauge__wrapper js-gauge-wrapper"
:class="`c-gauge--${gaugeType}`"
>
<template v-if="typeDial">
<svg
width="0"
height="0"
class="c-dial__clip-paths"
>
<defs>
<clipPath
id="gaugeBgMask"
clipPathUnits="objectBoundingBox"
>
<path d="M0.853553 0.853553C0.944036 0.763071 1 0.638071 1 0.5C1 0.223858 0.776142 0 0.5 0C0.223858 0 0 0.223858 0 0.5C0 0.638071 0.0559644 0.763071 0.146447 0.853553L0.285934 0.714066C0.23115 0.659281 0.197266 0.583598 0.197266 0.5C0.197266 0.332804 0.332804 0.197266 0.5 0.197266C0.667196 0.197266 0.802734 0.332804 0.802734 0.5C0.802734 0.583598 0.76885 0.659281 0.714066 0.714066L0.853553 0.853553Z" />
</clipPath>
<clipPath
id="gaugeValueMask"
clipPathUnits="objectBoundingBox"
>
<path d="M0.18926 0.81074C0.109735 0.731215 0.0605469 0.621351 0.0605469 0.5C0.0605469 0.257298 0.257298 0.0605469 0.5 0.0605469C0.742702 0.0605469 0.939453 0.257298 0.939453 0.5C0.939453 0.621351 0.890265 0.731215 0.81074 0.81074L0.714066 0.714066C0.76885 0.659281 0.802734 0.583599 0.802734 0.5C0.802734 0.332804 0.667196 0.197266 0.5 0.197266C0.332804 0.197266 0.197266 0.332804 0.197266 0.5C0.197266 0.583599 0.23115 0.659281 0.285934 0.714066L0.18926 0.81074Z" />
</clipPath>
</defs>
</svg>
<svg
class="c-dial__range c-gauge__range js-gauge-dial-range"
viewBox="0 0 512 512"
>
<text
v-if="displayMinMax"
font-size="35"
transform="translate(105 455) rotate(-45)"
>{{ rangeLow }}</text>
<text
v-if="displayMinMax"
font-size="35"
transform="translate(407 455) rotate(45)"
text-anchor="end"
>{{ rangeHigh }}</text>
</svg>
<svg
class="c-dial__current-value-text-wrapper"
viewBox="0 0 512 512"
>
<svg
v-if="displayCurVal"
class="c-dial__current-value-text-sizer"
:viewBox="curValViewBox"
>
<text
class="c-dial__current-value-text js-dial-current-value"
lengthAdjust="spacing"
text-anchor="middle"
style="transform: translate(50%, 70%)"
>{{ curVal }}</text>
</svg>
</svg>
<svg
class="c-dial__bg"
viewBox="0 0 10 10"
>
<g
v-if="limitLow !== null && dialLowLimitDeg < getLimitDegree('low', 'max')"
class="c-dial__limit-low"
:style="`transform: rotate(${dialLowLimitDeg}deg)`"
>
<rect
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q1')"
class="c-dial__low-limit__low"
x="5"
y="5"
width="5"
height="5"
/>
<rect
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q2')"
class="c-dial__low-limit__mid"
x="5"
y="0"
width="5"
height="5"
/>
<rect
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q3')"
class="c-dial__low-limit__high"
x="0"
y="0"
width="5"
height="5"
/>
</g>
<g
v-if="limitHigh !== null && dialHighLimitDeg < getLimitDegree('high', 'max')"
class="c-dial__limit-high"
:style="`transform: rotate(${dialHighLimitDeg}deg)`"
>
<rect
v-if="dialHighLimitDeg <= getLimitDegree('high', 'max')"
class="c-dial__high-limit__low"
x="0"
y="5"
width="5"
height="5"
/>
<rect
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q2')"
class="c-dial__high-limit__mid"
x="0"
y="0"
width="5"
height="5"
/>
<rect
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q3')"
class="c-dial__high-limit__high"
x="5"
y="0"
width="5"
height="5"
/>
</g>
</svg>
<svg
v-if="typeFilledDial"
class="c-dial__filled-value-wrapper"
viewBox="0 0 10 10"
>
<g
class="c-dial__filled-value"
:style="`transform: rotate(${degValueFilledDial}deg)`"
>
<rect
v-if="degValue >= getLimitDegree('low', 'q1')"
class="c-dial__filled-value__low"
x="5"
y="5"
width="5"
height="5"
/>
<rect
v-if="degValue >= getLimitDegree('low', 'q2')"
class="c-dial__filled-value__mid"
x="5"
y="0"
width="5"
height="5"
/>
<rect
v-if="degValue >= getLimitDegree('low', 'q3')"
class="c-dial__filled-value__high"
x="0"
y="0"
width="5"
height="5"
/>
</g>
</svg>
<svg
v-if="valueInBounds && typeNeedleDial"
class="c-dial__needle-value-wrapper"
viewBox="0 0 10 10"
>
<g
class="c-dial__needle-value"
:style="`transform: rotate(${degValue}deg)`"
>
<path d="M4.90234 9.39453L5.09766 9.39453L5.30146 8.20874C6.93993 8.05674 8.22265 6.67817 8.22266 5C8.22266 3.22018 6.77982 1.77734 5 1.77734C3.22018 1.77734 1.77734 3.22018 1.77734 5C1.77734 6.67817 3.06007 8.05674 4.69854 8.20874L4.90234 9.39453Z" />
</g>
</svg>
</template>
<template v-if="typeMeter">
<div class="c-meter">
<div
v-if="displayMinMax"
class="c-gauge__range c-meter__range js-gauge-meter-range"
>
<div class="c-meter__range__high">{{ rangeHigh }}</div>
<div class="c-meter__range__low">{{ rangeLow }}</div>
</div>
<div class="c-meter__bg">
<template v-if="typeMeterVertical">
<div
class="c-meter__value"
:style="`transform: translateY(${meterValueToPerc}%)`"
></div>
<div
v-if="limitHigh !== null && meterHighLimitPerc > 0"
class="c-meter__limit-high"
:style="`height: ${meterHighLimitPerc}%`"
></div>
<div
v-if="limitLow !== null && meterLowLimitPerc > 0"
class="c-meter__limit-low"
:style="`height: ${meterLowLimitPerc}%`"
></div>
</template>
<template v-if="typeMeterHorizontal">
<div
class="c-meter__value"
:style="`transform: translateX(${meterValueToPerc * -1}%)`"
></div>
<div
v-if="limitHigh !== null && meterHighLimitPerc > 0"
class="c-meter__limit-high"
:style="`width: ${meterHighLimitPerc}%`"
></div>
<div
v-if="limitLow !== null && meterLowLimitPerc > 0"
class="c-meter__limit-low"
:style="`width: ${meterLowLimitPerc}%`"
></div>
</template>
<svg
class="c-meter__current-value-text-wrapper"
viewBox="0 0 512 512"
>
<svg
v-if="displayCurVal"
class="c-meter__current-value-text-sizer"
:viewBox="curValViewBox"
preserveAspectRatio="xMidYMid meet"
>
<text
class="c-dial__current-value-text js-meter-current-value"
lengthAdjust="spacing"
text-anchor="middle"
style="transform: translate(50%, 70%)"
>{{ curVal }}</text>
</svg>
</svg>
</div>
</div>
</template>
</div>
</template>
<script>
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
const LIMIT_PADDING_IN_PERCENT = 10;
export default {
name: 'Gauge',
inject: ['openmct', 'domainObject', 'composition'],
data() {
let gaugeController = this.domainObject.configuration.gaugeController;
return {
curVal: 0,
digits: 3,
precision: gaugeController.precision,
displayMinMax: gaugeController.isDisplayMinMax,
displayCurVal: gaugeController.isDisplayCurVal,
limitHigh: gaugeController.limitHigh,
limitLow: gaugeController.limitLow,
rangeHigh: gaugeController.max,
rangeLow: gaugeController.min,
gaugeType: gaugeController.gaugeType,
activeTimeSystem: this.openmct.time.timeSystem()
};
},
computed: {
degValue() {
return this.percentToDegrees(this.valToPercent(this.curVal));
},
degValueFilledDial() {
if (this.curVal > this.rangeHigh) {
return this.percentToDegrees(100);
}
return this.percentToDegrees(this.valToPercent(this.curVal));
},
dialHighLimitDeg() {
return this.percentToDegrees(this.valToPercent(this.limitHigh));
},
dialLowLimitDeg() {
return this.percentToDegrees(this.valToPercent(this.limitLow));
},
curValViewBox() {
const DIGITS_RATIO = 10;
const VIEWBOX_STR = '0 0 X 15';
return VIEWBOX_STR.replace('X', this.digits * DIGITS_RATIO);
},
typeDial() {
return this.matchGaugeType('dial');
},
typeFilledDial() {
return this.matchGaugeType('dial-filled');
},
typeNeedleDial() {
return this.matchGaugeType('dial-needle');
},
typeMeter() {
return this.matchGaugeType('meter');
},
typeMeterHorizontal() {
return this.matchGaugeType('horizontal');
},
typeMeterVertical() {
return this.matchGaugeType('vertical');
},
typeMeterInverted() {
return this.matchGaugeType('inverted');
},
meterValueToPerc() {
const meterDirection = (this.typeMeterInverted) ? -1 : 1;
if (this.curVal <= this.rangeLow) {
return meterDirection * 100;
}
if (this.curVal >= this.rangeHigh) {
return 0;
}
return this.valToPercentMeter(this.curVal) * meterDirection;
},
meterHighLimitPerc() {
return this.valToPercentMeter(this.limitHigh);
},
meterLowLimitPerc() {
return 100 - this.valToPercentMeter(this.limitLow);
},
valueInBounds() {
return (this.curVal >= this.rangeLow && this.curVal <= this.rangeHigh);
},
timeFormatter() {
const timeSystem = this.activeTimeSystem;
const metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
return this.openmct.telemetry.getValueFormatter(metadataValue);
}
},
watch: {
curVal(newCurValue) {
if (this.digits < newCurValue.toString().length) {
this.digits = newCurValue.toString().length;
}
}
},
mounted() {
this.composition.on('add', this.addedToComposition);
this.composition.on('remove', this.removeTelemetryObject);
this.composition.load();
this.openmct.time.on('bounds', this.refreshData);
this.openmct.time.on('timeSystem', this.setTimeSystem);
},
destroyed() {
this.composition.off('add', this.addedToComposition);
this.composition.off('remove', this.removeTelemetryObject);
if (this.unsubscribe) {
this.unsubscribe();
}
this.openmct.time.off('bounds', this.refreshData);
this.openmct.time.off('timeSystem', this.setTimeSystem);
},
methods: {
getLimitDegree: getLimitDegree,
addTelemetryObjectAndSubscribe(domainObject) {
this.telemetryObject = domainObject;
this.request();
this.subscribe();
},
addedToComposition(domainObject) {
if (this.telemetryObject) {
this.confirmRemoval(domainObject);
} else {
this.addTelemetryObjectAndSubscribe(domainObject);
}
},
confirmRemoval(domainObject) {
const dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will replace the current telemetry source. Do you want to continue?',
buttons: [
{
label: 'Ok',
emphasis: true,
callback: () => {
this.removeFromComposition();
this.removeTelemetryObject();
this.addTelemetryObjectAndSubscribe(domainObject);
dialog.dismiss();
}
},
{
label: 'Cancel',
callback: () => {
this.removeFromComposition(domainObject);
dialog.dismiss();
}
}
]
});
},
matchGaugeType(str) {
return this.gaugeType.indexOf(str) !== -1;
},
percentToDegrees(vPercent) {
return this.round(((vPercent / 100) * 270) + DIAL_VALUE_DEG_OFFSET, 2);
},
removeFromComposition(telemetryObject = this.telemetryObject) {
let composition = this.domainObject.composition.filter(id =>
!this.openmct.objects.areIdsEqual(id, telemetryObject.identifier)
);
this.openmct.objects.mutate(this.domainObject, 'composition', composition);
},
refreshData(bounds, isTick) {
if (!isTick) {
this.request();
}
},
removeTelemetryObject() {
if (this.unsubscribe) {
this.unsubscribe();
this.unsubscribe = null;
}
this.metadata = null;
this.formats = null;
this.valueKey = null;
this.limitHigh = null;
this.limitLow = null;
this.rangeHigh = null;
this.rangeLow = null;
},
request(domainObject = this.telemetryObject) {
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
const LimitEvaluator = this.openmct.telemetry.getLimits(domainObject);
LimitEvaluator.limits().then(this.updateLimits);
this.valueKey = this
.metadata
.valuesForHints(['range'])[0].source;
this.openmct
.telemetry
.request(domainObject, { strategy: 'latest' })
.then(values => {
const length = values.length;
this.updateValue(values[length - 1]);
});
},
round(val, decimals = this.precision) {
let precision = Math.pow(10, decimals);
return Math.round(val * precision) / precision;
},
setTimeSystem(timeSystem) {
this.activeTimeSystem = timeSystem;
},
subscribe(domainObject = this.telemetryObject) {
this.unsubscribe = this.openmct
.telemetry
.subscribe(domainObject, this.updateValue.bind(this));
},
updateLimits(telemetryLimit) {
if (!telemetryLimit || !this.domainObject.configuration.gaugeController.isUseTelemetryLimits) {
return;
}
let limits = {
high: 0,
low: 0
};
if (telemetryLimit.CRITICAL) {
limits = telemetryLimit.CRITICAL;
} else if (telemetryLimit.DISTRESS) {
limits = telemetryLimit.DISTRESS;
} else if (telemetryLimit.SEVERE) {
limits = telemetryLimit.SEVERE;
} else if (telemetryLimit.WARNING) {
limits = telemetryLimit.WARNING;
} else if (telemetryLimit.WATCH) {
limits = telemetryLimit.WATCH;
} else {
this.openmct.notifications.error('No limits definition for given telemetry');
}
this.limitHigh = this.round(limits.high[this.valueKey]);
this.limitLow = this.round(limits.low[this.valueKey]);
this.rangeHigh = this.round(this.limitHigh + this.limitHigh * LIMIT_PADDING_IN_PERCENT / 100);
this.rangeLow = this.round(this.limitLow - Math.abs(this.limitLow * LIMIT_PADDING_IN_PERCENT / 100));
},
updateValue(datum) {
this.datum = datum;
if (this.isRendering) {
return;
}
const { start, end } = this.openmct.time.bounds();
const parsedValue = this.timeFormatter.parse(this.datum);
const beforeStartOfBounds = parsedValue < start;
const afterEndOfBounds = parsedValue > end;
if (afterEndOfBounds || beforeStartOfBounds) {
return;
}
this.isRendering = true;
requestAnimationFrame(() => {
this.isRendering = false;
this.curVal = this.round(this.formats[this.valueKey].format(this.datum), this.precision);
});
},
valToPercent(vValue) {
// Used by dial
if (vValue >= this.rangeHigh && this.typeFilledDial) {
// For filled dial, clip values over the high range to prevent over-rotation
return 100;
}
return ((vValue - this.rangeLow) / (this.rangeHigh - this.rangeLow)) * 100;
},
valToPercentMeter(vValue) {
return this.round((this.rangeHigh - vValue) / (this.rangeHigh - this.rangeLow) * 100, 2);
}
}
};
</script>

View File

@@ -1,171 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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>
<span class="form-control">
<span
class="field control"
:class="model.cssClass"
>
<ToggleSwitch
:id="'gaugeToggle'"
:checked="isUseTelemetryLimits"
label="Use telemetry limits for minimum and maximum ranges"
@change="toggleUseTelemetryLimits"
/>
<div
v-if="!isUseTelemetryLimits"
class="c-form--sub-grid"
>
<div class="c-form__row">
<span class="req-indicator req">
</span>
<label>Range minimum value</label>
<input
ref="min"
v-model.number="min"
data-field-name="min"
type="number"
@input="onChange"
>
</div>
<div class="c-form__row">
<span class="req-indicator">
</span>
<label>Range low limit</label>
<input
ref="limitLow"
v-model.number="limitLow"
data-field-name="limitLow"
type="number"
@input="onChange"
>
</div>
<div class="c-form__row">
<span class="req-indicator req">
</span>
<label>Range maximum value</label>
<input
ref="max"
v-model.number="max"
data-field-name="max"
type="number"
@input="onChange"
>
</div>
<div class="c-form__row">
<span class="req-indicator">
</span>
<label>Range high limit</label>
<input
ref="limitHigh"
v-model.number="limitHigh"
data-field-name="limitHigh"
type="number"
@input="onChange"
>
</div>
</div>
</span>
</span>
</template>
<script>
import ToggleSwitch from '@/ui/components/ToggleSwitch.vue';
export default {
components: {
ToggleSwitch
},
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
isUseTelemetryLimits: this.model.value.isUseTelemetryLimits,
isDisplayMinMax: this.model.value.isDisplayMinMax,
isDisplayCurVal: this.model.value.isDisplayCurVal,
limitHigh: this.model.value.limitHigh,
limitLow: this.model.value.limitLow,
max: this.model.value.max,
min: this.model.value.min
};
},
methods: {
onChange(event) {
const data = {
model: this.model,
value: {
gaugeType: this.model.value.gaugeType,
isDisplayMinMax: this.isDisplayMinMax,
isDisplayCurVal: this.isDisplayCurVal,
isUseTelemetryLimits: this.isUseTelemetryLimits,
limitLow: this.limitLow,
limitHigh: this.limitHigh,
max: this.max,
min: this.min,
precision: this.model.value.precision
}
};
if (event) {
const target = event.target;
const targetIndicator = target.parentElement.querySelector('.req-indicator');
if (targetIndicator.classList.contains('req')) {
targetIndicator.classList.add('visited');
}
this.model.validate(data, (valid) => {
Object.entries(valid).forEach(([key, isValid]) => {
const element = this.$refs[key];
const reqIndicatorElement = element.parentElement.querySelector('.req-indicator');
reqIndicatorElement.classList.toggle('invalid', !isValid);
if (reqIndicatorElement.classList.contains('req') && (!isValid || reqIndicatorElement.classList.contains('visited'))) {
reqIndicatorElement.classList.toggle('valid', isValid);
}
});
});
}
this.$emit('onChange', data);
},
toggleUseTelemetryLimits() {
this.isUseTelemetryLimits = !this.isUseTelemetryLimits;
this.onChange();
},
toggleMinMax() {
this.isDisplayMinMax = !this.isDisplayMinMax;
this.onChange();
}
}
};
</script>

View File

@@ -1,39 +0,0 @@
const GAUGE_LIMITS = {
q1: 0,
q2: 90,
q3: 180,
q4: 270
};
export const DIAL_VALUE_DEG_OFFSET = 45;
// type: low, high
// quadrant: low, mid, high, max
export function getLimitDegree(type, quadrant) {
if (quadrant === 'max') {
return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET;
}
return type === 'low'
? getLowLimitDegree(quadrant)
: getHighLimitDegree(quadrant)
;
}
function getLowLimitDegree(quadrant) {
return GAUGE_LIMITS[quadrant] + DIAL_VALUE_DEG_OFFSET;
}
function getHighLimitDegree(quadrant) {
if (quadrant === 'q1') {
return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET;
}
if (quadrant === 'q2') {
return GAUGE_LIMITS.q3 + DIAL_VALUE_DEG_OFFSET;
}
if (quadrant === 'q3') {
return GAUGE_LIMITS.q2 + DIAL_VALUE_DEG_OFFSET;
}
}

View File

@@ -1,223 +0,0 @@
.is-object-type-gauge {
overflow: hidden;
}
.req-indicator {
width: 20px;
&.invalid,
&.invalid.req { @include validationState($glyph-icon-x, $colorFormInvalid); }
&.valid,
&.valid.req { @include validationState($glyph-icon-check, $colorFormValid); }
&.req { @include validationState($glyph-icon-asterisk, $colorFormRequired); }
}
.c-gauge {
// Both dial and meter types
overflow: hidden;
&__range {
$c: $colorGaugeRange;
color: $c;
text {
fill: $c;
}
}
&__wrapper {
@include abs();
overflow: hidden;
}
}
/********************************************** DIAL GAUGE */
svg[class*='c-dial'] {
max-height: 100%;
max-width: 100%;
position: absolute;
g {
transform-origin: center;
}
}
.c-dial {
&__bg {
background: $colorGaugeBg;
clip-path: url(#gaugeBgMask);
}
&__limit-high rect { fill: $colorGaugeLimitHigh; }
&__limit-low rect { fill: $colorGaugeLimitLow; }
&__filled-value-wrapper {
clip-path: url(#gaugeValueMask);
}
&__needle-value-wrapper {
clip-path: url(#gaugeValueMask);
}
&__filled-value { fill: $colorGaugeValue; }
&__needle-value {
fill: $colorGaugeValue;
transition: transform $transitionTimeGauge;
}
&__current-value-text {
fill: $colorGaugeTextValue;
font-family: $heroFont;
}
}
/********************************************** METER GAUGE */
.c-meter {
// Common styles for c-meter
@include abs();
display: flex;
svg {
// current-value-text
position: absolute;
height: 100%;
width: 100%;
}
&__range {
display: flex;
flex: 0 0 auto;
justify-content: space-between;
}
&__bg {
background: $colorGaugeBg;
border-radius: $basicCr;
flex: 1 1 auto;
overflow: hidden;
}
&__value {
// Filled area
position: absolute;
background: $colorGaugeValue;
transition: transform $transitionTimeGauge;
z-index: 1;
}
.c-gauge__curval {
fill: $colorGaugeMeterTextValue !important;
}
[class*='limit'] {
position: absolute;
}
&__limit-high {
background: $colorGaugeLimitHigh;
}
&__limit-low {
background: $colorGaugeLimitLow;
}
}
.c-meter {
.c-gauge--meter-vertical &,
.c-gauge--meter-vertical-inverted & {
&__range {
flex-direction: column;
min-width: min-content;
margin-right: $interiorMarginSm;
text-align: right;
}
&__value {
// Filled area
$lrM: $marginGaugeMeterValue;
left: $lrM;
right: $lrM;
top: 0;
bottom: 0;
}
[class*='limit'] {
left: 0;
right: 0;
}
}
.c-gauge--meter-vertical & {
&__limit-low {
bottom: 0;
}
&__limit-high {
top: 0;
}
}
.c-gauge--meter-vertical-inverted & {
&__limit-low {
top: 0;
}
&__limit-high {
bottom: 0;
}
&__range__low {
order: 1;
}
&__range__high {
order: 2;
}
}
.c-gauge--meter-horizontal & {
flex-direction: column;
&__range {
flex-direction: row;
min-height: min-content;
margin-top: $interiorMarginSm;
order: 2;
&__high {
order: 2;
}
&__low {
order: 1;
}
}
&__bg {
order: 1;
}
&__value {
// Filled area
$m: $marginGaugeMeterValue;
top: $m;
bottom: $m;
left: 0;
right: 0;
}
[class*='limit'] {
top: 0;
bottom: 0;
}
&__limit-low {
left: 0;
}
&__limit-high {
right: 0;
}
}
}

View File

@@ -2,16 +2,11 @@ import ImageryViewComponent from './components/ImageryView.vue';
import Vue from 'vue';
const DEFAULT_IMAGE_FRESHNESS_OPTIONS = {
fadeOutDelayTime: '0s',
fadeOutDurationTime: '30s'
};
export default class ImageryView {
constructor(openmct, domainObject, objectPath, options) {
constructor(openmct, domainObject, objectPath) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;
this.options = options;
this.component = undefined;
}
@@ -32,7 +27,6 @@ export default class ImageryView {
openmct: this.openmct,
domainObject: this.domainObject,
objectPath: alternateObjectPath || this.objectPath,
imageFreshnessOptions: this.options?.imageFreshness || DEFAULT_IMAGE_FRESHNESS_OPTIONS,
currentView: this
},
data() {

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import ImageryView from './ImageryView';
export default function ImageryViewProvider(openmct, options) {
export default function ImageryViewProvider(openmct) {
const type = 'example.imagery';
function hasImageTelemetry(domainObject) {
@@ -43,7 +43,7 @@ export default function ImageryViewProvider(openmct, options) {
return hasImageTelemetry(domainObject) && (!isChildOfTimeStrip || openmct.router.isNavigatedObject(objectPath));
},
view: function (domainObject, objectPath) {
return new ImageryView(openmct, domainObject, objectPath, options);
return new ImageryView(openmct, domainObject, objectPath);
}
};
}

View File

@@ -14,7 +14,7 @@ $elemBg: rgba(black, 0.7);
position: absolute;
left: 0;
top: 0;
z-index: 2;
z-index: 1;
@include userSelectNone;
}

View File

@@ -132,10 +132,6 @@
<!-- image fresh -->
<div
v-if="canTrackDuration"
:style="{
'animation-delay': imageFreshnessOptions.fadeOutDelayTime,
'animation-duration': imageFreshnessOptions.fadeOutDurationTime
}"
:class="{'c-imagery--new': isImageNew && !refreshCSS}"
class="c-imagery__age icon-timer"
>{{ formattedDuration }}</div>
@@ -143,13 +139,13 @@
<!-- spacecraft position fresh -->
<div
v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh"
class="c-imagery__age icon-check c-imagery--new no-animation"
class="c-imagery__age icon-check c-imagery--new"
>POS</div>
<!-- camera position fresh -->
<div
v-if="relatedTelemetry.hasRelatedTelemetry && isCameraPositionFresh"
class="c-imagery__age icon-check c-imagery--new no-animation"
class="c-imagery__age icon-check c-imagery--new"
>CAM</div>
</div>
<div class="h-local-controls">
@@ -163,13 +159,10 @@
</div>
</div>
<div
v-if="displayThumbnails"
class="c-imagery__thumbs-wrapper"
:class="[
{ 'is-paused': isPaused && !isFixed },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused },
{ 'is-small-thumbs': displayThumbnailsSmall },
{ 'hide': !displayThumbnails }
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
>
<div
@@ -182,7 +175,6 @@
:key="image.url + image.time"
class="c-imagery__thumb c-thumb"
:class="{ selected: focusedImageIndex === index && isPaused }"
:title="image.formattedTime"
@click="thumbnailClicked(index)"
>
<a
@@ -236,8 +228,6 @@ const ARROW_LEFT = 37;
const SCROLL_LATENCY = 250;
const ZOOM_SCALE_DEFAULT = 1;
const SHOW_THUMBS_THRESHOLD_HEIGHT = 200;
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
export default {
components: {
@@ -245,7 +235,7 @@ export default {
ImageControls
},
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
props: {
focusedImageTimestamp: {
type: Number,
@@ -278,7 +268,6 @@ export default {
imageContainerHeight: undefined,
sizedImageWidth: 0,
sizedImageHeight: 0,
viewHeight: 0,
lockCompass: true,
resizingWindow: false,
timeContext: undefined,
@@ -297,8 +286,7 @@ export default {
imageTranslateY: 0,
pan: undefined,
animateZoom: true,
imagePanned: false,
forceShowThumbnails: false
imagePanned: false
};
},
computed: {
@@ -314,15 +302,6 @@ export default {
return compassRoseSizingClasses;
},
displayThumbnails() {
return (
this.forceShowThumbnails
|| this.viewHeight >= SHOW_THUMBS_THRESHOLD_HEIGHT
);
},
displayThumbnailsSmall() {
return this.viewHeight > SHOW_THUMBS_THRESHOLD_HEIGHT && this.viewHeight <= SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT;
},
time() {
return this.formatTime(this.focusedImage);
},
@@ -331,16 +310,6 @@ export default {
},
isImageNew() {
let cutoff = FIVE_MINUTES;
if (this.imageFreshnessOptions) {
const { fadeOutDelayTime, fadeOutDurationTime} = this.imageFreshnessOptions;
// convert css duration to IS8601 format for parsing
const isoFormattedDuration = 'PT' + fadeOutDurationTime.toUpperCase();
const isoFormattedDelay = 'PT' + fadeOutDelayTime.toUpperCase();
const parsedDuration = moment.duration(isoFormattedDuration).asMilliseconds();
const parsedDelay = moment.duration(isoFormattedDelay).asMilliseconds();
cutoff = parsedDuration + parsedDelay;
}
let age = this.numericDuration;
return age < cutoff && !this.refreshCSS;
@@ -524,8 +493,6 @@ export default {
if (!this.isPaused) {
this.setFocusedImage(imageIndex);
this.scrollToRight();
} else {
this.scrollToFocused();
}
},
deep: true
@@ -565,7 +532,7 @@ export default {
this.updateRelatedTelemetryForFocusedImage = _.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
// for resizing the object view
this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400, { leading: true });
this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400);
if (this.$refs.imageBG) {
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
@@ -612,9 +579,6 @@ export default {
},
methods: {
calculateViewHeight() {
this.viewHeight = this.$el.clientHeight;
},
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
@@ -988,8 +952,6 @@ export default {
}
this.setSizedImageDimensions();
this.calculateViewHeight();
this.scrollToFocused();
},
setSizedImageDimensions() {
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
@@ -1018,8 +980,6 @@ export default {
this.scrollToRight('reset');
}
this.calculateViewHeight();
this.$nextTick(() => {
this.resizingWindow = false;
});

View File

@@ -1,15 +1,3 @@
@use 'sass:math';
@keyframes fade-out {
from {
background-color: rgba($colorOk, 0.5);
}
to {
background-color: rgba($colorOk, 0);
color: inherit;
}
}
.c-imagery {
display: flex;
flex-direction: column;
@@ -135,20 +123,12 @@
// New imagery
$bgColor: $colorOk;
color: $colorOkFg;
background-color: rgba($bgColor, 0.5);
animation-name: fade-out;
animation-timing-function: ease-in;
animation-iteration-count: 1;
animation-fill-mode: forwards;
&.no-animation {
animation: none;
background: rgba($bgColor, 0.5);
@include flash($animName: flashImageAge, $iter: 2, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
}
}
&__thumbs-wrapper {
display: flex; // Uses row layout
justify-content: flex-end;
&.is-autoscroll-off {
background: $colorInteriorBorder;
@@ -166,11 +146,17 @@
flex: 0 1 auto;
display: flex;
flex-direction: row;
height: 145px;
height: 135px;
overflow-x: auto;
overflow-y: hidden;
margin-bottom: 1px;
padding-bottom: $interiorMarginSm;
.c-thumb:last-child {
// Hilite the lastest thumb
background: $colorBodyFg;
color: $colorBodyBg;
}
}
&__auto-scroll-resume-button {
@@ -183,12 +169,10 @@
/*************************************** THUMBS */
.c-thumb {
$w: $imageThumbsD;
display: flex;
flex-direction: column;
padding: 4px;
min-width: $w;
width: $w;
width: $imageThumbsD;
&:hover {
background: $colorThumbHoverBg;
@@ -210,20 +194,12 @@
}
}
.is-small-thumbs {
.l-layout,
.c-fl {
.c-imagery__thumbs-scroll-area {
height: 60px; // Allow room for scrollbar
}
.c-thumb {
$w: math.div($imageThumbsD, 2);
min-width: $w;
width: $w;
&__timestamp {
// When Imagery is in a layout, hide the thumbs area
display: none;
}
}
}
/*************************************** IMAGERY LOCAL CONTROLS*/
@@ -231,7 +207,7 @@
.h-local-controls--overlay-content {
position: absolute;
left: $interiorMargin; top: $interiorMargin;
z-index: 70;
z-index: 2;
background: $colorLocalControlOvrBg;
border-radius: $basicCr;
max-width: 250px;

View File

@@ -23,9 +23,9 @@
import ImageryViewProvider from './ImageryViewProvider';
import ImageryTimestripViewProvider from './ImageryTimestripViewProvider';
export default function (options) {
export default function () {
return function install(openmct) {
openmct.objectViews.addProvider(new ImageryViewProvider(openmct, options));
openmct.objectViews.addProvider(new ImageryViewProvider(openmct));
openmct.objectViews.addProvider(new ImageryTimestripViewProvider(openmct));
};
}

View File

@@ -344,8 +344,6 @@ describe("The Imagery View Layouts", () => {
);
openmct.install(clearDataPlugin);
clearDataAction = openmct.actions.getAction('clear-data-action');
// force show the thumbnails
imageryView._getInstance().$children[0].forceShowThumbnails = true;
return Vue.nextTick();
});
@@ -525,10 +523,7 @@ describe("The Imagery View Layouts", () => {
expect(clearDataAction).toBeDefined();
});
it('on clearData action should clear data for object is selected', async (done) => {
// force show the thumbnails
imageryView._getInstance().$children[0].forceShowThumbnails = true;
await Vue.nextTick();
it('on clearData action should clear data for object is selected', (done) => {
expect(parent.querySelectorAll('.c-imagery__thumb').length).not.toBe(0);
openmct.objectViews.on('clearData', async (_domainObject) => {
await Vue.nextTick();

View File

@@ -35,8 +35,6 @@
ref="searchResults"
:domain-object="domainObject"
:results="searchResults"
@cancelEdit="cancelTransaction"
@editingEntry="startTransaction"
@changeSectionPage="changeSelectedSection"
@updateEntries="updateEntries"
/>
@@ -142,8 +140,6 @@
:selected-page="selectedPage"
:selected-section="selectedSection"
:read-only="false"
@cancelEdit="cancelTransaction"
@editingEntry="startTransaction"
@deleteEntry="deleteEntry"
@updateEntry="updateEntry"
/>
@@ -714,8 +710,6 @@ export default {
notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;
mutateObject(this.openmct, this.domainObject, 'configuration.entries', notebookEntries);
this.saveTransaction();
},
getPageIdFromUrl() {
return this.openmct.router.getParams().pageId;
@@ -752,39 +746,6 @@ export default {
this.selectPage(pageId);
this.syncUrlWithPageAndSection();
},
activeTransaction() {
return this.openmct.objects.getActiveTransaction();
},
startTransaction() {
if (!this.openmct.editor.isEditing()) {
this.openmct.objects.startTransaction();
}
},
saveTransaction() {
const transaction = this.activeTransaction();
if (!transaction || this.openmct.editor.isEditing()) {
return;
}
return transaction.commit()
.catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
},
cancelTransaction() {
if (!this.openmct.editor.isEditing()) {
const transaction = this.activeTransaction();
transaction.cancel()
.catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
}
}
}
};

View File

@@ -55,7 +55,6 @@
class="c-ne__text c-ne__input"
tabindex="0"
contenteditable
@focus="editingEntry()"
@blur="updateEntryValue($event)"
@keydown.enter.exact.prevent
@keyup.enter.exact.prevent="forceBlur($event)"
@@ -285,16 +284,11 @@ export default {
this.$emit('updateEntry', this.entry);
},
editingEntry() {
this.$emit('editingEntry');
},
updateEntryValue($event) {
const value = $event.target.innerText;
if (value !== this.entry.text && value.match(/\S/)) {
this.entry.text = value;
this.$emit('updateEntry', this.entry);
} else {
this.$emit('cancelEdit');
}
}
}

View File

@@ -33,8 +33,6 @@
:read-only="true"
:selected-page="result.page"
:selected-section="result.section"
@editingEntry="editingEntry"
@cancelEdit="cancelEdit"
@changeSectionPage="changeSectionPage"
@updateEntries="updateEntries"
/>
@@ -65,12 +63,6 @@ export default {
}
},
methods: {
editingEntry() {
this.$emit('editingEntry');
},
cancelEdit() {
this.$emit('cancelEdit');
},
changeSectionPage(data) {
this.$emit('changeSectionPage', data);
},

View File

@@ -20,7 +20,10 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import PerformancePlugin from './plugin.js';
import { createOpenMct, resetApplicationState } from 'utils/testing';
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
describe('the plugin', () => {
let openmct;
@@ -28,8 +31,9 @@ describe('the plugin', () => {
let child;
let performanceIndicator;
let countFramesPromise;
beforeEach(done => {
beforeEach((done) => {
openmct = createOpenMct();
element = document.createElement('div');
@@ -38,9 +42,11 @@ describe('the plugin', () => {
openmct.install(new PerformancePlugin());
countFramesPromise = countFrames();
openmct.on('start', done);
performanceIndicator = openmct.indicators.indicatorObjects.find(indicator => {
performanceIndicator = openmct.indicators.indicatorObjects.find((indicator) => {
return indicator.text && indicator.text() === '~ fps';
});
@@ -55,21 +61,25 @@ describe('the plugin', () => {
expect(performanceIndicator).toBeDefined();
});
it('calculates an fps value', async () => {
await loopForABit();
// eslint-disable-next-line
expect(parseInt(performanceIndicator.text().split(' fps')[0])).toBeGreaterThan(0);
it('correctly calculates fps', () => {
return countFramesPromise.then((frames) => {
expect(performanceIndicator.text()).toEqual(`${frames} fps`);
});
});
function loopForABit() {
function countFrames() {
let startTime = performance.now();
let frames = 0;
return new Promise(resolve => {
requestAnimationFrame(function loop() {
if (++frames === 240) {
resolve();
return new Promise((resolve) => {
requestAnimationFrame(function incrementCount() {
let now = performance.now();
if ((now - startTime) < 1000) {
frames++;
requestAnimationFrame(incrementCount);
} else {
requestAnimationFrame(loop);
resolve(frames);
}
});
});

View File

@@ -37,7 +37,6 @@
v-if="seriesModels.length > 0"
:tick-width="tickWidth"
:single-series="seriesModels.length === 1"
:has-same-range-value="hasSameRangeValue"
:series-model="seriesModels[0]"
:style="{
left: (plotWidth - tickWidth) + 'px'
@@ -251,8 +250,7 @@ export default {
loaded: false,
isTimeOutOfSync: false,
showLimitLineLabels: undefined,
isFrozenOnMouseDown: false,
hasSameRangeValue: true
isFrozenOnMouseDown: false
};
},
computed: {
@@ -364,7 +362,6 @@ export default {
this.setDisplayRange(series, xKey);
}, this);
this.listenTo(series, 'change:yKey', () => {
this.checkSameRangeValue();
this.loadSeriesData(series);
}, this);
@@ -372,18 +369,10 @@ export default {
this.loadSeriesData(series);
}, this);
this.checkSameRangeValue();
this.loadSeriesData(series);
},
checkSameRangeValue() {
this.hasSameRangeValue = this.seriesModels.every((model) => {
return model.get('yKey') === this.seriesModels[0].get('yKey');
});
},
removeSeries(plotSeries) {
this.checkSameRangeValue();
this.stopListening(plotSeries);
},
@@ -499,7 +488,7 @@ export default {
},
setDisplayRange(series, xKey) {
if (this.config.series.models.length !== 1) {
if (this.config.series.length !== 1) {
return;
}

View File

@@ -31,7 +31,7 @@
>
<div
v-for="(tick, i) in ticks"
:key="'tick-left' + i"
:key="i"
class="gl-plot-tick gl-plot-x-tick-label"
:style="{
left: (100 * (tick.value - min) / interval) + '%'
@@ -47,7 +47,7 @@
>
<div
v-for="(tick, i) in ticks"
:key="'tick-top' + i"
:key="i"
class="gl-plot-tick gl-plot-y-tick-label"
:style="{ top: (100 * (max - tick.value) / interval) + '%' }"
:title="tick.fullText || tick.text"
@@ -60,7 +60,7 @@
<template v-if="position === 'right'">
<div
v-for="(tick, i) in ticks"
:key="'tick-right' + i"
:key="i"
class="gl-plot-hash hash-v"
:style="{
right: (100 * (max - tick.value) / interval) + '%',
@@ -72,7 +72,7 @@
<template v-if="position === 'bottom'">
<div
v-for="(tick, i) in ticks"
:key="'tick-bottom' + i"
:key="i"
class="gl-plot-hash hash-h"
:style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }"
>
@@ -192,6 +192,7 @@ export default {
if (this.axisType === 'yAxis' && this.axis.get('logMode')) {
return getLogTicks(range.min, range.max, number, 4);
// return getLogTicks2(range.min, range.max, number);
} else {
return ticks(range.min, range.max, number);
}
@@ -203,7 +204,6 @@ export default {
updateTicks(forceRegeneration = false) {
const range = this.axis.get('displayRange');
if (!range) {
delete this.min;
delete this.max;

View File

@@ -29,9 +29,9 @@
>
<div
v-if="canShowYAxisLabel"
v-if="singleSeries"
class="gl-plot-label gl-plot-y-label"
:class="{'icon-gear': (yKeyOptions.length > 1 && singleSeries)}"
:class="{'icon-gear': (yKeyOptions.length > 1)}"
>{{ yAxisLabel }}
</div>
@@ -76,12 +76,6 @@ export default {
return true;
}
},
hasSameRangeValue: {
type: Boolean,
default() {
return true;
}
},
seriesModel: {
type: Object,
default() {
@@ -101,11 +95,6 @@ export default {
loaded: false
};
},
computed: {
canShowYAxisLabel() {
return this.singleSeries === true || this.hasSameRangeValue === true;
}
},
mounted() {
this.yAxis = this.getYAxisFromConfig();
this.loaded = true;

View File

@@ -279,7 +279,7 @@ export default {
// Have to throw away the old canvas elements and replace with new
// canvas elements in order to get new drawing contexts.
const div = document.createElement('div');
div.innerHTML = this.canvasTemplate + this.canvasTemplate;
div.innerHTML = this.TEMPLATE;
const mainCanvas = div.querySelectorAll("canvas")[1];
const overlayCanvas = div.querySelectorAll("canvas")[0];
this.canvas.parentNode.replaceChild(mainCanvas, this.canvas);

View File

@@ -157,8 +157,7 @@ export default class PlotConfigurationModel extends Model {
@typedef {{
configuration: {
series: import('./PlotSeries').PlotSeriesModelType[]
yAxis: import('./YAxisModel').YAxisModelType
},
}
}} SomeDomainObject_NeedsName
*/

View File

@@ -256,12 +256,12 @@ export default class PlotSeries extends Model {
this.evaluate = function (datum) {
return this.limitEvaluator.evaluate(datum, valueMetadata);
}.bind(this);
this.set('unit', valueMetadata.unit);
const format = this.formats[newKey];
this.getYVal = (value) => {
const scale = 1; // TODO get from UI, positive number above 0
const y = format.parse(value);
return this.logMode ? symlog(y, 10) : y;
return this.logMode ? scale * symlog(y, 10) : y;
};
}

View File

@@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import Model from './Model';
import Model from "./Model";
/**
* @extends {Model<XAxisModelType, XAxisModelOptions>}
@@ -49,11 +49,11 @@ export default class XAxisModel extends Model {
}
});
this.on('change:frozen', (frozen) => {
this.on('change:frozen', ((frozen) => {
if (!frozen) {
this.set('range', this.get('range'));
}
});
}));
if (this.get('range')) {
this.set('range', this.get('range'));
@@ -126,7 +126,7 @@ export default class XAxisModel extends Model {
/**
@typedef {import("./Model").ModelType<{
range?: NumberRange
range: NumberRange
displayRange: NumberRange
frozen: boolean
label: string

View File

@@ -64,17 +64,22 @@ export default class YAxisModel extends Model {
*/
listenToSeriesCollection(seriesCollection) {
this.seriesCollection = seriesCollection;
this.listenTo(this.seriesCollection, 'add', series => {
this.listenTo(this.seriesCollection, 'add', (series => {
this.trackSeries(series);
this.updateFromSeries(this.seriesCollection);
}, this);
this.listenTo(this.seriesCollection, 'remove', series => {
}), this);
this.listenTo(this.seriesCollection, 'remove', (series => {
this.untrackSeries(series);
this.updateFromSeries(this.seriesCollection);
}, this);
}), this);
this.seriesCollection.forEach(this.trackSeries, this);
this.updateFromSeries(this.seriesCollection);
}
updateDisplayRange(range) {
if (!this.get('autoscale')) {
this.set('displayRange', range);
}
}
toggleFreeze(frozen) {
if (!frozen) {
this.toggleAutoscale(this.get('autoscale'));
@@ -136,11 +141,11 @@ export default class YAxisModel extends Model {
}
resetStats() {
this.unset('stats');
this.seriesCollection.forEach(series => {
this.seriesCollection.forEach(function (series) {
if (series.has('stats')) {
this.updateStats(series.get('stats'));
}
});
}, this);
}
/**
* @param {import('./PlotSeries').default} series
@@ -162,82 +167,24 @@ export default class YAxisModel extends Model {
this.resetStats();
this.updateFromSeries(this.seriesCollection);
}
/**
* This is called in order to map the user-provided `range` to the
* `displayRange` that we actually use for plot display.
*
* @param {import('./XAxisModel').NumberRange} range
*/
updateDisplayRange(range) {
if (this.get('autoscale')) {
return;
}
const _range = { ...range };
if (this.get('logMode')) {
_range.min = symlog(range.min, 10);
_range.max = symlog(range.max, 10);
}
this.set('displayRange', _range);
}
/**
* @param {boolean} autoscale
*/
toggleAutoscale(autoscale) {
if (autoscale && this.has('stats')) {
this.set('displayRange', this.applyPadding(this.get('stats')));
return;
}
const range = this.get('range');
if (range) {
// If we already have a user-defined range, make sure it maps to the
// range we'll actually use for the ticks.
const _range = { ...range };
if (this.get('logMode')) {
_range.min = symlog(range.min, 10);
_range.max = symlog(range.max, 10);
}
this.set('displayRange', _range);
} else {
// Otherwise use the last known displayRange as the initial
// values for the user-defined range, so that we don't end up
// with any error from an undefined user range.
const _range = this.get('displayRange');
if (!_range) {
return;
}
if (this.get('logMode')) {
_range.min = antisymlog(_range.min, 10);
_range.max = antisymlog(_range.max, 10);
}
this.set('range', _range);
this.set('displayRange', this.get('range'));
}
}
/** @param {boolean} logMode */
onLogModeChange(logMode) {
const range = this.get('displayRange');
const scale = 1; // TODO get from UI, positive number above 0
if (logMode) {
range.min = symlog(range.min, 10);
range.max = symlog(range.max, 10);
range.min = scale * symlog(range.min, 10);
range.max = scale * symlog(range.max, 10);
} else {
range.min = antisymlog(range.min, 10);
range.max = antisymlog(range.max, 10);
range.min = antisymlog(range.min / scale, 10);
range.max = antisymlog(range.max / scale, 10);
}
this.set('displayRange', range);
@@ -271,18 +218,19 @@ export default class YAxisModel extends Model {
const yKey = sampleSeries.get('yKey');
const yMetadata = sampleSeries.metadata.value(yKey);
const yFormat = sampleSeries.formats[yKey];
const scale = 1; // TODO get from UI, positive number above 0
if (this.get('logMode')) {
this.set('format', (n) => yFormat.format(antisymlog(n, 10)));
this.set('format', (n) => yFormat.format(antisymlog(n / scale, 10)));
} else {
this.set('format', (n) => yFormat.format(n));
}
this.set('values', yMetadata.values);
if (!label) {
const labelName = seriesCollection
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).name : ''))
.reduce((a, b) => {
const labelName = seriesCollection.map(function (s) {
return s.metadata ? s.metadata.value(s.get('yKey')).name : '';
}).reduce(function (a, b) {
if (a === undefined) {
return b;
}
@@ -300,9 +248,9 @@ export default class YAxisModel extends Model {
return;
}
const labelUnits = seriesCollection
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).units : ''))
.reduce((a, b) => {
const labelUnits = seriesCollection.map(function (s) {
return s.metadata ? s.metadata.value(s.get('yKey')).units : '';
}).reduce(function (a, b) {
if (a === undefined) {
return b;
}
@@ -324,18 +272,15 @@ export default class YAxisModel extends Model {
/**
* @override
* @param {import('./Model').ModelOptions<YAxisModelType, YAxisModelOptions>} options
* @returns {Partial<YAxisModelType>}
* @returns {YAxisModelType}
*/
defaultModel(options) {
// @ts-ignore incomplete YAxisModelType object used for default value.
return {
frozen: false,
autoscale: true,
logMode: options.model?.logMode ?? false,
autoscalePadding: 0.1
// 'range' is not specified here, it is undefined at first. When the
// user turns off autoscale, the current 'displayRange' is used for
// the initial value of 'range'.
};
}
}
@@ -347,7 +292,7 @@ export default class YAxisModel extends Model {
autoscale: boolean
logMode: boolean
autoscalePadding: number
stats?: import('./XAxisModel').NumberRange
stats: import('./XAxisModel').NumberRange
values: Array<TODO>
}} YAxisModelType
*/

View File

@@ -65,10 +65,10 @@
class="l-inspector-part"
>
<div
v-show="!autoscale && validationErrors.range"
v-show="!autoscale && validation.range"
class="grid-span-all form-error"
>
{{ validationErrors.range }}
{{ validation.range }}
</div>
<li class="grid-row force-border">
<div
@@ -101,7 +101,7 @@
</template>
<script>
import { objectPath } from "./formUtil";
import { objectPath, validate } from "./formUtil";
import _ from "lodash";
export default {
@@ -122,7 +122,7 @@ export default {
autoscalePadding: '',
rangeMin: '',
rangeMax: '',
validationErrors: {}
validation: {}
};
},
mounted() {
@@ -150,16 +150,19 @@ export default {
range: {
objectPath: 'configuration.yAxis.range',
coerce: function coerceRange(range) {
const newRange = {
min: -1,
max: 1
if (!range) {
return {
min: 0,
max: 0
};
}
if (range && typeof range.min !== 'undefined' && range.min !== null) {
const newRange = {};
if (typeof range.min !== 'undefined' && range.min !== null) {
newRange.min = Number(range.min);
}
if (range && typeof range.max !== 'undefined' && range.max !== null) {
if (typeof range.max !== 'undefined' && range.max !== null) {
newRange.max = Number(range.max);
}
@@ -189,6 +192,12 @@ export default {
if (Number(range.min) > Number(range.max)) {
return 'Minimum must be less than Maximum.';
}
if (model.get('autoscale')) {
return false;
}
return true;
}
}
};
@@ -198,9 +207,14 @@ export default {
this.autoscale = this.yAxis.get('autoscale');
this.logMode = this.yAxis.get('logMode');
this.autoscalePadding = this.yAxis.get('autoscalePadding');
const range = this.yAxis.get('range') ?? this.yAxis.get('displayRange');
this.rangeMin = range?.min;
this.rangeMax = range?.max;
const range = this.yAxis.get('range');
if (!range) {
this.rangeMin = undefined;
this.rangeMax = undefined;
} else {
this.rangeMin = range.min;
this.rangeMax = range.max;
}
},
updateForm(formKey) {
let newVal;
@@ -216,18 +230,21 @@ export default {
let oldVal = this.yAxis.get(formKey);
const formField = this.fields[formKey];
const validationError = formField.validate?.(newVal, this.yAxis);
this.validationErrors[formKey] = validationError;
if (validationError) {
const path = objectPath(formField.objectPath);
const validationResult = validate(newVal, this.yAxis, formField.validate);
if (validationResult === true) {
delete this.validation[formKey];
} else {
this.validation[formKey] = validationResult;
return;
}
newVal = formField.coerce?.(newVal) ?? newVal;
oldVal = formField.coerce?.(oldVal) ?? oldVal;
const path = objectPath(formField.objectPath);
if (!_.isEqual(newVal, oldVal)) {
// TODO: Why do we mutate yAxis twice, once directly, once via objects.mutate? Or are they different objects?
// TODO: Why do we mutate yAxis twice, once directly, once via objects.mutate?
this.yAxis.set(formKey, newVal);
if (path) {
this.openmct.objects.mutate(

View File

@@ -15,5 +15,15 @@ export function validate(value, model, validateFunc) {
}
export function objectPath(path) {
return path && typeof path !== 'function' ? () => path : path;
if (path) {
if (typeof path !== "function") {
const staticObjectPath = path;
return function (object, model) {
return staticObjectPath;
};
}
return path;
}
}

View File

@@ -389,7 +389,7 @@ describe("the plugin", function () {
expect(xAxisElement.length).toBe(1);
let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
expect(ticks.length).toBe(9);
expect(ticks.length).toBe(5);
done();
});
@@ -694,7 +694,7 @@ describe("the plugin", function () {
Vue.nextTick(() => {
let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
expect(ticks.length).toBe(9);
expect(ticks.length).toBe(5);
done();
});
@@ -1086,9 +1086,7 @@ describe("the plugin", function () {
expandControl.dispatchEvent(clickEvent);
const yAxisProperties = editOptionsEl.querySelectorAll("div.grid-properties:first-of-type .l-inspector-part");
// TODO better test
expect(yAxisProperties.length).toEqual(2);
expect(yAxisProperties.length).toEqual(3);
});
it('renders color palette options', () => {

View File

@@ -132,6 +132,12 @@ export default {
}
}
});
if (this.object && this.object.type === 'conditionWidget' && keys.includes('output')) {
this.openmct.objects.mutate(this.object, 'conditionalLabel', styleObj.output);
} else {
this.openmct.objects.mutate(this.object, 'conditionalLabel', '');
}
}
}
};

View File

@@ -47,7 +47,8 @@ export function getLogTicks(start, stop, mainTickCount = 8, secondaryTickCount =
const mainLogTicks = ticks(start, stop, mainTickCount);
// original values
const mainTicks = mainLogTicks.map(n => antisymlog(n, 10));
const scale = 1; // TODO get from UI, positive number above 0
const mainTicks = mainLogTicks.map(n => antisymlog(n / scale, 10));
const result = [];
@@ -68,7 +69,7 @@ export function getLogTicks(start, stop, mainTickCount = 8, secondaryTickCount =
nextTick - rangeBetweenMainTicks / (secondaryTickCount + 1),
secondaryTickCount - 2
)
.map(n => symlog(n, 10));
.map(n => scale * symlog(n, 10));
result.push(...secondaryLogTicks);
@@ -78,6 +79,13 @@ export function getLogTicks(start, stop, mainTickCount = 8, secondaryTickCount =
return result;
}
export function getLogTicks2(start, stop, count = 8) {
const scale = 1; // TODO get from UI, positive number above 0
return ticks(antisymlog(start / scale, 10), antisymlog(stop / scale, 10), count)
.map(n => scale * symlog(n, 10));
}
/**
* Linear tick generation from d3-array.
*/

View File

@@ -76,9 +76,7 @@ define([
'./timer/plugin',
'./userIndicator/plugin',
'../../example/exampleUser/plugin',
'./localStorage/plugin',
'./gauge/GaugePlugin',
'./timelist/plugin'
'./localStorage/plugin'
], function (
_,
UTCTimeSystem,
@@ -135,9 +133,7 @@ define([
Timer,
UserIndicator,
ExampleUser,
LocalStorage,
GaugePlugin,
TimeList
LocalStorage
) {
const plugins = {};
@@ -214,8 +210,6 @@ define([
plugins.DeviceClassifier = DeviceClassifier.default;
plugins.UserIndicator = UserIndicator.default;
plugins.LocalStorage = LocalStorage.default;
plugins.Gauge = GaugePlugin.default;
plugins.Timelist = TimeList.default;
return plugins;
});

View File

@@ -9,11 +9,7 @@ export default class TelemetryTableView {
this.objectPath = objectPath;
this.component = undefined;
Object.defineProperty(this, 'table', {
value: new TelemetryTable(domainObject, openmct),
enumerable: false,
configurable: false
});
this.table = new TelemetryTable(domainObject, openmct);
}
getViewContext() {

View File

@@ -29,6 +29,10 @@ define(
_,
EventEmitter
) {
const LESS_THAN = -1;
const EQUAL = 0;
const GREATER_THAN = 1;
/**
* @constructor
*/
@@ -76,7 +80,10 @@ define(
this.rows = [];
}
this.sortAndMergeRows(rowsToAdd);
for (let row of rowsToAdd) {
let index = this.sortedIndex(this.rows, row);
this.rows.splice(index, 0, row);
}
// we emit filter no matter what to trigger
// an update of visible rows
@@ -85,85 +92,58 @@ define(
}
}
sortAndMergeRows(rows) {
const sortedRowsToAdd = this.sortCollection(rows);
sortedLastIndex(rows, testRow) {
return this.sortedIndex(rows, testRow, _.sortedLastIndex);
}
/**
* Finds the correct insertion point for the given row.
* Leverages lodash's `sortedIndex` function which implements a binary search.
* @private
*/
sortedIndex(rows, testRow, lodashFunction = _.sortedIndexBy) {
if (this.rows.length === 0) {
this.rows = sortedRowsToAdd;
return;
return 0;
}
const firstIncomingRow = sortedRowsToAdd[0];
const lastIncomingRow = sortedRowsToAdd[sortedRowsToAdd.length - 1];
const firstExistingRow = this.rows[0];
const lastExistingRow = this.rows[this.rows.length - 1];
if (this.firstRowInSortOrder(lastIncomingRow, firstExistingRow)
=== lastIncomingRow
) {
this.rows = [...sortedRowsToAdd, ...this.rows];
} else if (this.firstRowInSortOrder(lastExistingRow, firstIncomingRow)
=== lastExistingRow
) {
this.rows = [...this.rows, ...sortedRowsToAdd];
} else {
this.mergeSortedRows(sortedRowsToAdd);
}
}
sortCollection(rows) {
const sortedRows = _.orderBy(
rows,
row => row.getParsedValue(this.sortOptions.key), this.sortOptions.direction
);
return sortedRows;
}
mergeSortedRows(rows) {
const mergedRows = [];
let i = 0;
let j = 0;
while (i < this.rows.length && j < rows.length) {
const existingRow = this.rows[i];
const incomingRow = rows[j];
if (this.firstRowInSortOrder(existingRow, incomingRow) === existingRow) {
mergedRows.push(existingRow);
i++;
} else {
mergedRows.push(incomingRow);
j++;
}
}
// tail of existing rows is all that is left to merge
if (i < this.rows.length) {
for (i; i < this.rows.length; i++) {
mergedRows.push(this.rows[i]);
}
}
// tail of incoming rows is all that is left to merge
if (j < rows.length) {
for (j; j < rows.length; j++) {
mergedRows.push(rows[j]);
}
}
this.rows = mergedRows;
}
firstRowInSortOrder(row1, row2) {
const val1 = this.getValueForSortColumn(row1);
const val2 = this.getValueForSortColumn(row2);
const testRowValue = this.getValueForSortColumn(testRow);
const firstValue = this.getValueForSortColumn(this.rows[0]);
const lastValue = this.getValueForSortColumn(this.rows[this.rows.length - 1]);
if (this.sortOptions.direction === 'asc') {
return val1 <= val2 ? row1 : row2;
if (testRowValue > lastValue) {
return this.rows.length;
} else if (testRowValue === lastValue) {
// Maintain stable sort
return this.rows.length;
} else if (testRowValue <= firstValue) {
return 0;
} else {
return val1 >= val2 ? row1 : row2;
return lodashFunction(rows, testRow, (thisRow) => {
return this.getValueForSortColumn(thisRow);
});
}
} else {
if (testRowValue >= firstValue) {
return 0;
} else if (testRowValue < lastValue) {
return this.rows.length;
} else if (testRowValue === lastValue) {
// Maintain stable sort
return this.rows.length;
} else {
// Use a custom comparison function to support descending sort.
return lodashFunction(rows, testRow, (thisRow) => {
const thisRowValue = this.getValueForSortColumn(thisRow);
if (testRowValue === thisRowValue) {
return EQUAL;
} else if (testRowValue < thisRowValue) {
return LESS_THAN;
} else {
return GREATER_THAN;
}
});
}
}
}
@@ -225,9 +205,8 @@ define(
sortBy(sortOptions) {
if (arguments.length > 0) {
this.sortOptions = sortOptions;
performance.mark('table:row:sort:start');
this.rows = _.orderBy(this.rows, (row) => row.getParsedValue(sortOptions.key), sortOptions.direction);
performance.mark('table:row:sort:stop');
this.emit('sort');
}

View File

@@ -613,7 +613,6 @@ export default {
this.calculateScrollbarWidth();
},
sortBy(columnKey) {
performance.mark('table:sort');
// If sorting by the same column, flip the sort direction.
if (this.sortOptions.key === columnKey) {
if (this.sortOptions.direction === 'asc') {
@@ -670,7 +669,6 @@ export default {
this.setHeight();
},
rowsAdded(rows) {
performance.mark('row:added');
this.setHeight();
let sizingRow;
@@ -692,7 +690,6 @@ export default {
this.updateVisibleRows();
},
rowsRemoved(rows) {
performance.mark('row:removed');
this.setHeight();
this.updateVisibleRows();
},

View File

@@ -1,394 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2022, 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
ref="timelistHolder"
class="c-timelist"
>
<list-view
:items="planActivities"
:header-items="headerItems"
:default-sort="defaultSort"
class="sticky"
/>
</div>
</template>
<script>
import {getValidatedPlan} 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 moment from "moment";
import uuid from "uuid";
const SCROLL_TIMEOUT = 10000;
const ROW_HEIGHT = 30;
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss:SSS';
const headerItems = [
{
defaultDirection: true,
isSortable: true,
property: 'start',
name: 'Start Time',
format: function (value, object) {
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`;
}
}, {
defaultDirection: false,
property: 'duration',
name: 'Time To/From',
format: function (value) {
let result;
if (value < 0) {
result = `-${getPreciseDuration(Math.abs(value))}`;
} else if (value > 0) {
result = `+${getPreciseDuration(value)}`;
} else {
result = 'Now';
}
return result;
}
}, {
defaultDirection: true,
property: 'name',
name: 'Activity'
}
];
const defaultSort = {
property: 'start',
defaultDirection: true
};
export default {
components: {
ListView
},
inject: ['openmct', 'domainObject', 'path'],
data() {
return {
viewBounds: undefined,
height: 0,
planActivities: [],
headerItems: headerItems,
defaultSort: defaultSort
};
},
mounted() {
this.isEditing = this.openmct.editor.isEditing();
this.timestamp = Date.now();
this.getPlanDataAndSetConfig(this.domainObject);
this.unlisten = this.openmct.objects.observe(this.domainObject, 'selectFile', this.getPlanDataAndSetConfig);
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.openmct.editor.on('isEditing', this.setEditState);
this.deferAutoScroll = _.debounce(this.deferAutoScroll, 500);
this.$el.parentElement.addEventListener('scroll', this.deferAutoScroll, true);
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
if (this.unlistenConfig) {
this.unlistenConfig();
}
if (this.unlistenTicker) {
this.unlistenTicker();
}
if (this.removeStatusListener) {
this.removeStatusListener();
}
this.openmct.editor.off('isEditing', this.setEditState);
this.$el.parentElement.removeEventListener('scroll', this.deferAutoScroll, true);
if (this.clearAutoScrollDisabledTimer) {
clearTimeout(this.clearAutoScrollDisabledTimer);
}
},
methods: {
getPlanDataAndSetConfig(mutatedObject) {
this.getPlanData(mutatedObject);
this.setViewFromConfig(mutatedObject.configuration);
},
setViewFromConfig(configuration) {
if (this.isEditing) {
this.filterValue = configuration.filter;
this.hideAll = false;
this.showAll = true;
this.listActivities();
} else {
this.filterValue = configuration.filter;
this.setSort();
this.setViewBounds();
this.listActivities();
}
},
getPlanData(domainObject) {
this.planData = getValidatedPlan(domainObject);
},
setViewBounds() {
const pastEventsIndex = this.domainObject.configuration.pastEventsIndex;
const currentEventsIndex = this.domainObject.configuration.currentEventsIndex;
const futureEventsIndex = this.domainObject.configuration.futureEventsIndex;
const pastEventsDuration = this.domainObject.configuration.pastEventsDuration;
const pastEventsDurationIndex = this.domainObject.configuration.pastEventsDurationIndex;
const futureEventsDuration = this.domainObject.configuration.futureEventsDuration;
const futureEventsDurationIndex = this.domainObject.configuration.futureEventsDurationIndex;
if (pastEventsIndex === 0 && futureEventsIndex === 0 && currentEventsIndex === 0) {
//show all events
this.showAll = false;
this.viewBounds = undefined;
this.hideAll = true;
return;
}
this.hideAll = false;
if (pastEventsIndex === 1 && futureEventsIndex === 1 && currentEventsIndex === 1) {
//show all events
this.showAll = true;
this.viewBounds = undefined;
return;
}
this.showAll = false;
this.viewBounds = {};
this.noCurrent = currentEventsIndex === 0;
if (pastEventsIndex !== 1) {
const pastDurationInMS = this.getDurationInMilliSeconds(pastEventsDuration, pastEventsDurationIndex);
this.viewBounds.pastEnd = (timestamp) => {
if (pastEventsIndex === 2) {
return timestamp - pastDurationInMS;
} else if (pastEventsIndex === 0) {
return timestamp + 1;
}
};
}
if (futureEventsIndex !== 1) {
const futureDurationInMS = this.getDurationInMilliSeconds(futureEventsDuration, futureEventsDurationIndex);
this.viewBounds.futureStart = (timestamp) => {
if (futureEventsIndex === 2) {
return timestamp + futureDurationInMS;
} else if (futureEventsIndex === 0) {
return 0;
}
};
}
},
getDurationInMilliSeconds(duration, durationIndex) {
if (duration > 0) {
if (durationIndex === 0) {
return duration * 1000;
} else if (durationIndex === 1) {
return duration * 60 * 1000;
} else if (durationIndex === 2) {
return duration * 60 * 60 * 1000;
}
}
},
listActivities() {
let groups = Object.keys(this.planData);
let activities = [];
groups.forEach((key) => {
activities = activities.concat(this.planData[key]);
});
activities = activities.filter(this.filterActivities);
activities = this.applyStyles(activities);
this.setScrollTop();
// sort by start time
this.planActivities = activities.sort(this.sortByStartTime);
},
clearPreviousActivities(time) {
if (time instanceof Date) {
this.timestamp = time.getTime();
} else {
this.timestamp = time;
}
this.listActivities();
},
filterActivities(activity, index) {
const hasFilterMatch = this.filterByName(activity.name);
if (hasFilterMatch === false || this.hideAll === true) {
return false;
}
if (this.showAll === true) {
return true;
}
//current event or future start event or past end event
const isCurrent = (this.noCurrent === false && this.timestamp >= activity.start && this.timestamp <= activity.end);
const isPast = (this.timestamp > activity.end && (this.viewBounds.pastEnd === undefined || activity.end >= this.viewBounds.pastEnd(this.timestamp)));
const isFuture = (this.timestamp < activity.start && (this.viewBounds.futureStart === undefined || activity.start <= this.viewBounds.futureStart(this.timestamp)));
return isCurrent || isPast || isFuture;
},
filterByName(name) {
const filters = this.filterValue.split(',');
return filters.some((search => {
const normalized = search.trim().toLowerCase();
const regex = new RegExp(normalized);
return regex.test(name.toLowerCase());
}));
},
applyStyles(activities) {
let firstCurrentActivityIndex = -1;
let currentActivitiesCount = 0;
const styledActivities = activities.map((activity, index) => {
if (this.timestamp >= activity.start && this.timestamp <= activity.end) {
activity.cssClass = '--is-current';
if (firstCurrentActivityIndex < 0) {
firstCurrentActivityIndex = index;
}
currentActivitiesCount = currentActivitiesCount + 1;
} else if (this.timestamp < activity.start) {
activity.cssClass = '--is-future';
} else {
activity.cssClass = '--is-past';
}
if (!activity.key) {
activity.key = uuid();
}
activity.duration = activity.start - this.timestamp;
return activity;
});
this.firstCurrentActivityIndex = firstCurrentActivityIndex;
this.currentActivitiesCount = currentActivitiesCount;
return styledActivities;
},
canAutoScroll() {
//this distinguishes between programmatic vs user-triggered scroll events
this.autoScrolled = (this.dontAutoScroll !== true);
return this.autoScrolled;
},
resetScroll() {
if (this.canAutoScroll() === false) {
return;
}
this.firstCurrentActivityIndex = -1;
this.currentActivitiesCount = 0;
this.$el.parentElement.scrollTo({top: 0});
this.autoScrolled = false;
},
setScrollTop() {
//scroll to somewhere mid-way of the current activities
if (this.firstCurrentActivityIndex > -1) {
if (this.canAutoScroll() === false) {
return;
}
const scrollOffset = this.currentActivitiesCount > 0 ? Math.floor(this.currentActivitiesCount / 2) : 0;
this.$el.parentElement.scrollTo({
top: ROW_HEIGHT * (this.firstCurrentActivityIndex + scrollOffset),
behavior: "smooth"
});
this.autoScrolled = false;
} else {
this.resetScroll();
}
},
deferAutoScroll() {
//if this is not a user-triggered event, don't defer auto scrolling
if (this.autoScrolled) {
this.autoScrolled = false;
return;
}
this.dontAutoScroll = true;
const self = this;
if (this.clearAutoScrollDisabledTimer) {
clearTimeout(this.clearAutoScrollDisabledTimer);
}
this.clearAutoScrollDisabledTimer = setTimeout(() => {
self.dontAutoScroll = false;
self.setScrollTop();
}, SCROLL_TIMEOUT);
},
setSort() {
const sortOrder = SORT_ORDER_OPTIONS[this.domainObject.configuration.sortOrderIndex];
const property = sortOrder.property;
const direction = sortOrder.direction.toLowerCase() === 'asc';
this.defaultSort = {
property,
defaultDirection: direction
};
},
sortByStartTime(a, b) {
const numA = parseInt(a.start, 10);
const numB = parseInt(b.start, 10);
return numA - numB;
},
setStatus(status) {
this.status = status;
},
setEditState(isEditing) {
this.isEditing = isEditing;
this.setViewFromConfig(this.domainObject.configuration);
}
}
};
</script>

View File

@@ -1,67 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 Timelist from './Timelist.vue';
import { TIMELIST_TYPE } from './constants';
import Vue from 'vue';
export default function TimelistViewProvider(openmct) {
return {
key: 'timelist.view',
name: 'Time List',
cssClass: 'icon-timelist',
canView(domainObject) {
return domainObject.type === TIMELIST_TYPE;
},
canEdit(domainObject) {
return domainObject.type === TIMELIST_TYPE;
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
Timelist
},
provide: {
openmct,
domainObject,
path: objectPath
},
template: '<timelist></timelist>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@@ -1,24 +0,0 @@
export const SORT_ORDER_OPTIONS = [
{
label: 'Start ascending',
property: 'start',
direction: 'ASC'
},
{
label: 'Start descending',
property: 'start',
direction: 'DESC'
},
{
label: 'End ascending',
property: 'end',
direction: 'ASC'
},
{
label: 'End descending',
property: 'end',
direction: 'DESC'
}
];
export const TIMELIST_TYPE = 'timelist';

View File

@@ -1,124 +0,0 @@
<template>
<li class="c-inspect-properties__row">
<div
class="c-inspect-properties__label"
title="Options for future events."
>{{ label }}</div>
<div
v-if="canEdit"
class="c-inspect-properties__value"
>
<select
v-model="index"
@change="updateForm('index')"
>
<option
v-for="(activityOption, activityKey) in activitiesOptions"
:key="activityKey"
:value="activityKey"
>{{ activityOption }}</option>
</select>
<input
v-if="index === 2"
v-model="duration"
class="c-input c-input--sm"
type="text"
@change="updateForm('duration')"
>
<select
v-if="index === 2"
v-model="durationIndex"
@change="updateForm('durationIndex')"
>
<option
v-for="(durationOption, durationKey) in durationOptions"
:key="durationKey"
:value="durationKey"
>{{ durationOption }}</option>
</select>
</div>
<div
v-else
class="c-inspect-properties__value"
>
{{ activitiesOptions[index] }} <span v-if="index > 1">{{ duration }} {{ durationOptions[durationIndex] }}</span>
</div>
</li>
</template>
<script>
const ACTIVITIES_OPTIONS = [
'Don\'t show',
'Show all',
'Show starts within',
'Show after end for'
];
const DURATION_OPTIONS = [
'seconds',
'minutes',
'hours'
];
export default {
inject: ['openmct', 'domainObject'],
props: {
label: {
type: String,
required: true
},
prefix: {
type: String,
required: true
}
},
data() {
return {
index: this.domainObject.configuration[`${this.prefix}Index`],
durationIndex: this.domainObject.configuration[`${this.prefix}DurationIndex`],
duration: this.domainObject.configuration[`${this.prefix}Duration`],
activitiesOptions: ACTIVITIES_OPTIONS,
durationOptions: DURATION_OPTIONS,
isEditing: this.openmct.editor.isEditing()
};
},
computed: {
canEdit() {
return this.isEditing && !this.domainObject.locked;
}
},
mounted() {
if (this.prefix === 'futureEvents') {
this.activitiesOptions = ACTIVITIES_OPTIONS.slice(0, 3);
} else if (this.prefix === 'pastEvents') {
this.activitiesOptions = ACTIVITIES_OPTIONS.filter((item, index) => index !== 2);
} else if (this.prefix === 'currentEvents') {
this.activitiesOptions = ACTIVITIES_OPTIONS.slice(0, 2);
}
this.openmct.editor.on('isEditing', this.setEditState);
},
beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState);
},
methods: {
updateForm(property) {
if (!this.isValid()) {
return;
}
const capitalized = property.charAt(0).toUpperCase() + property.substr(1);
this.$emit('updated', {
property: `${this.prefix}${capitalized}`,
value: this[property]
});
},
isValid() {
return this.index < 2 || (this.durationIndex >= 0 && this.duration > 0);
},
setEditState(isEditing) {
this.isEditing = isEditing;
}
}
};
</script>

View File

@@ -1,91 +0,0 @@
<template>
<li class="c-inspect-properties__row">
<div
v-if="canEdit"
class="c-inspect-properties__hint span-all"
>Filter this view by comma-separated keywords.</div>
<div
class="c-inspect-properties__label"
title="Filter by keyword."
>Filters</div>
<div
v-if="canEdit"
class="c-inspect-properties__value"
:class="{'form-error': hasError}"
>
<textarea
v-model="filterValue"
class="c-input--flex"
type="text"
@keydown.enter.exact.stop="forceBlur($event)"
@keyup="updateForm($event, 'filter')"
></textarea>
</div>
<div
v-else
class="c-inspect-properties__value"
>
{{ filterValue }}
</div>
</li>
</template>
<script>
export default {
inject: ['openmct', 'domainObject'],
data() {
return {
isEditing: this.openmct.editor.isEditing(),
filterValue: this.domainObject.configuration.filter,
hasError: false
};
},
computed: {
canEdit() {
return this.isEditing && !this.domainObject.locked;
}
},
mounted() {
this.openmct.editor.on('isEditing', this.setEditState);
},
beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState);
},
methods: {
setEditState(isEditing) {
this.isEditing = isEditing;
if (!this.isEditing && this.hasError) {
this.filterValue = this.domainObject.configuration.filter;
this.hasError = false;
}
},
forceBlur(event) {
event.target.blur();
},
updateForm(event, property) {
if (!this.isValid()) {
this.hasError = true;
return;
}
this.hasError = false;
this.$emit('updated', {
property,
value: this.filterValue.replace(/,(\s)*$/, '')
});
},
isValid() {
// Test for any word character, any whitespace character or comma
if (this.filterValue === '') {
return true;
}
const regex = new RegExp(/^([a-zA-Z0-9_\-\s,])+$/g);
return regex.test(this.filterValue);
}
}
};
</script>

View File

@@ -1,70 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 TimelistPropertiesView from "./TimelistPropertiesView.vue";
import { TIMELIST_TYPE } from '../constants';
import Vue from 'vue';
export default function TimeListInspectorViewProvider(openmct) {
return {
key: 'timelist-inspector',
name: 'Timelist Inspector View',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
}
let context = selection[0][0].context;
return context && context.item
&& context.item.type === TIMELIST_TYPE;
},
view: function (selection) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
TimelistPropertiesView: TimelistPropertiesView
},
provide: {
openmct,
domainObject: selection[0][0].context.item
},
template: '<timelist-properties-view></timelist-properties-view>'
});
},
destroy: function () {
if (component) {
component.$destroy();
component = undefined;
}
}
};
},
priority: function () {
return 1;
}
};
}

View File

@@ -1,146 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2022, 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="c-timelist-properties">
<div class="c-inspect-properties">
<ul class="c-inspect-properties__section">
<div
class="c-inspect-properties_header"
title="'Timeframe options'"
>Timeframe</div>
<li class="c-inspect-properties__row">
<div
v-if="canEdit"
class="c-inspect-properties__hint span-all"
>These settings are not previewed and will be applied after editing is completed.</div>
<div
class="c-inspect-properties__label"
title="Sort order of the timelist."
>Sort Order</div>
<div
v-if="canEdit"
class="c-inspect-properties__value"
>
<select
v-model="sortOrderIndex"
@change="updateSortOrder()"
>
<option
v-for="(sortOrderOption, index) in sortOrderOptions"
:key="index"
:value="index"
>{{ sortOrderOption.label }}</option>
</select>
</div>
<div
v-else
class="c-inspect-properties__value"
>
{{ sortOrderOptions[sortOrderIndex].label }}
</div>
</li>
<event-properties
v-for="type in eventTypes"
:key="type.prefix"
:label="type.label"
:prefix="type.prefix"
@updated="eventPropertiesUpdated"
/>
</ul>
</div>
<div class="c-inspect-properties">
<ul class="c-inspect-properties__section">
<div
class="c-inspect-properties_header"
title="'Filters'"
>Filtering</div>
<filtering @updated="eventPropertiesUpdated" />
</ul>
</div>
</div>
</template>
<script>
import EventProperties from './EventProperties.vue';
import { SORT_ORDER_OPTIONS } from '../constants';
import Filtering from './Filtering.vue';
const EVENT_TYPES = [
{
label: 'Future Events',
prefix: 'futureEvents'
},
{
label: 'Current Events',
prefix: 'currentEvents'
},
{
label: 'Past Events',
prefix: 'pastEvents'
}
];
export default {
components: {
Filtering,
EventProperties
},
inject: ['openmct', 'domainObject'],
data() {
return {
sortOrderIndex: this.domainObject.configuration.sortOrderIndex,
sortOrderOptions: SORT_ORDER_OPTIONS,
eventTypes: EVENT_TYPES,
isEditing: this.openmct.editor.isEditing()
};
},
computed: {
canEdit() {
return this.isEditing && !this.domainObject.locked;
}
},
mounted() {
this.openmct.editor.on('isEditing', this.setEditState);
},
beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState);
},
methods: {
setEditState(isEditing) {
this.isEditing = isEditing;
},
updateSortOrder() {
this.updateProperty('sortOrderIndex', this.sortOrderIndex);
},
updateProperty(key, value) {
this.openmct.objects.mutate(this.domainObject, `configuration.${key}`, value);
},
eventPropertiesUpdated(data) {
const key = data.property;
const value = data.value;
this.updateProperty(key, value);
}
}
};
</script>

View File

@@ -1,68 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 TimelistViewProvider from './TimelistViewProvider';
import { TIMELIST_TYPE } from './constants';
import TimeListInspectorViewProvider from "./inspector/TimeListInspectorViewProvider";
export default function () {
return function install(openmct) {
openmct.types.addType(TIMELIST_TYPE, {
name: 'Time List',
key: TIMELIST_TYPE,
description: 'A configurable, time-ordered list view of activities for a compatible mission plan file.',
creatable: true,
cssClass: 'icon-timelist',
form: [
{
name: 'Upload Plan (JSON File)',
key: 'selectFile',
control: 'file-input',
required: true,
text: 'Select File...',
type: 'application/json',
property: [
"selectFile"
]
}
],
initialize: function (domainObject) {
domainObject.configuration = {
sortOrderIndex: 0,
futureEventsIndex: 0,
futureEventsDurationIndex: 0,
futureEventsDuration: 20,
currentEventsIndex: 1,
currentEventsDurationIndex: 0,
currentEventsDuration: 20,
pastEventsIndex: 0,
pastEventsDurationIndex: 0,
pastEventsDuration: 20,
filter: ''
};
}
});
openmct.objectViews.addProvider(new TimelistViewProvider(openmct));
openmct.inspectorViews.addProvider(new TimeListInspectorViewProvider(openmct));
};
}

View File

@@ -1,181 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 TimelistPlugin from "./plugin";
import { TIMELIST_TYPE } from "./constants";
import Vue from 'vue';
import moment from "moment";
const LIST_ITEM_CLASS = '.js-table__body .js-list-item';
const LIST_ITEM_VALUE_CLASS = '.js-list-item__value';
const LIST_ITEM_BODY_CLASS = '.js-table__body th';
describe('the plugin', function () {
let timelistDefinition;
let element;
let child;
let openmct;
let appHolder;
let originalRouterPath;
beforeEach((done) => {
appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
openmct.install(new TimelistPlugin());
timelistDefinition = openmct.types.get(TIMELIST_TYPE).definition;
element = document.createElement('div');
element.style.width = '640px';
element.style.height = '480px';
child = document.createElement('div');
child.style.width = '640px';
child.style.height = '480px';
element.appendChild(child);
originalRouterPath = openmct.router.path;
openmct.on('start', done);
openmct.start(appHolder);
});
afterEach(() => {
openmct.router.path = originalRouterPath;
return resetApplicationState(openmct);
});
let mockTimelistObject = {
name: 'Timelist',
key: TIMELIST_TYPE,
creatable: true
};
it('defines a timelist object type with the correct key', () => {
expect(timelistDefinition.key).toEqual(mockTimelistObject.key);
});
it('is creatable', () => {
expect(timelistDefinition.creatable).toEqual(mockTimelistObject.creatable);
});
describe('the timelist view', () => {
it('provides a timelist view', () => {
const testViewObject = {
id: "test-object",
type: TIMELIST_TYPE
};
openmct.router.path = [testViewObject];
const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);
let timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');
expect(timelistView).toBeDefined();
});
});
describe('the timelist view displays activities', () => {
let timelistDomainObject;
let timelistView;
beforeEach(() => {
timelistDomainObject = {
identifier: {
key: 'test-object',
namespace: ''
},
type: TIMELIST_TYPE,
id: "test-object",
configuration: {
sortOrderIndex: 0,
futureEventsIndex: 1,
futureEventsDurationIndex: 0,
futureEventsDuration: 20,
currentEventsIndex: 1,
currentEventsDurationIndex: 0,
currentEventsDuration: 20,
pastEventsIndex: 1,
pastEventsDurationIndex: 0,
pastEventsDuration: 20,
filter: ''
},
selectFile: {
body: JSON.stringify({
"TEST-GROUP": [
{
"name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
"start": 1597170002854,
"end": 1597171032854,
"type": "TEST-GROUP",
"color": "fuchsia",
"textColor": "black"
},
{
"name": "Sed ut perspiciatis",
"start": 1597171132854,
"end": 1597171232854,
"type": "TEST-GROUP",
"color": "fuchsia",
"textColor": "black"
}
]
})
}
};
openmct.router.path = [timelistDomainObject];
const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);
timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');
let view = timelistView.view(timelistDomainObject, []);
view.show(child, true);
return Vue.nextTick();
});
it('displays the activities', () => {
const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(2);
});
it('displays the activity headers', () => {
const headers = element.querySelectorAll(LIST_ITEM_BODY_CLASS);
expect(headers.length).toEqual(4);
});
it('displays activity details', (done) => {
Vue.nextTick(() => {
const itemEls = element.querySelectorAll(LIST_ITEM_CLASS);
const itemValues = itemEls[0].querySelectorAll(LIST_ITEM_VALUE_CLASS);
expect(itemValues.length).toEqual(4);
expect(itemValues[3].innerHTML.trim()).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua');
expect(itemValues[0].innerHTML.trim()).toEqual(`${moment(1597170002854).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`);
expect(itemValues[1].innerHTML.trim()).toEqual(`${moment(1597171032854).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`);
done();
});
});
});
});

View File

@@ -1,55 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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.
*****************************************************************************/
.c-timelist {
& .nowMarker.hasCurrent {
height: 2px;
position: absolute;
z-index: 10;
background: cyan;
width: 100%;
}
.c-list-item {
/* Time Lists */
&.--is-current {
background-color: $colorCurrentBg;
border-top: 1px solid $colorCurrentBorder !important;
color: $colorCurrentFg;
font-weight: bold;
}
&.--is-future {
background-color: $colorFutureBg;
border-top-color: $colorFutureBorder !important;
color: $colorFutureFg;
}
&__value {
&.--duration {
width: 5%;
}
}
}
}

View File

@@ -38,41 +38,41 @@ export default class ViewLargeAction {
}
invoke(objectPath, view) {
performance.mark('viewlarge.start');
const childElement = view?.parentElement?.firstChild;
const parentElement = view.parentElement;
let childElement = parentElement && parentElement.firstChild;
if (!childElement) {
const message = "ViewLargeAction: missing element";
this.openmct.notifications.error(message);
throw new Error(message);
}
this._expand(objectPath, view);
this._expand(objectPath, childElement);
}
appliesTo(objectPath, view) {
const childElement = view?.parentElement?.firstChild;
return childElement && !childElement.classList.contains('js-main-container')
appliesTo(objectPath, view = {}) {
const parentElement = view.parentElement;
const element = parentElement && parentElement.firstChild;
const viewLargeAction = element && !element.classList.contains('js-main-container')
&& !this.openmct.router.isNavigatedObject(objectPath);
return viewLargeAction;
}
_expand(objectPath, view) {
const element = this._getPreview(objectPath, view);
_expand(objectPath, childElement) {
const parentElement = childElement.parentElement;
this.overlay = this.openmct.overlays.overlay({
element,
element: this._getPreview(objectPath),
size: 'large',
autoHide: false,
onDestroy: () => {
this.preview.$destroy();
this.preview = undefined;
delete this.preview;
onDestroy() {
parentElement.append(childElement);
}
});
}
_getPreview(objectPath, view) {
this.preview = new Vue({
_getPreview(objectPath) {
const preview = new Vue({
components: {
Preview
},
@@ -80,14 +80,9 @@ export default class ViewLargeAction {
openmct: this.openmct,
objectPath
},
data() {
return {
view
};
},
template: '<Preview :existing-view="view"></Preview>'
template: '<Preview></Preview>'
});
return this.preview.$mount().$el;
return preview.$mount().$el;
}
}

View File

@@ -345,7 +345,7 @@ $shdwItemText: none;
$colorTabBorder: pullForward($colorBodyBg, 10%);
$colorTabBodyBg: $colorBodyBg;
$colorTabBodyFg: pullForward($colorBodyFg, 20%);
$colorTabHeaderBg: #575757;
$colorTabHeaderBg: rgba($colorBodyFg, 0.15);
$colorTabHeaderFg: $colorBodyFg;
$colorTabHeaderBorder: $colorBodyBg;
$colorTabGroupHeaderBg: pullForward($colorBodyBg, 5%);
@@ -366,24 +366,6 @@ $legendHoverValueBg: rgba($colorBodyFg, 0.2);
$legendTableHeadBg: $colorTabHeaderBg;
$colorPlotLimitLineBg: rgba($colorBodyBg, 0.2);
// Gauges
$colorGaugeBg: pullForward($colorBodyBg, 5%); // Gauge radial area background, meter background
$colorGaugeValue: rgba(#fff, 0.3); // Gauge value graphic (radial sweep, bar) color
$colorGaugeTextValue: #fff; // Radial gauge text value
$colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid on value bar
$colorGaugeRange: $colorBodyFg; // Range text color
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4);
$colorGaugeLimitLow: $colorGaugeLimitHigh;
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
// Time Strip and Lists
$colorCurrentBg: rgba($colorStatusAlert, 0.3);
$colorCurrentFg: pullForward($colorBodyFg, 20%);
$colorCurrentBorder: $colorBodyBg;
$colorFutureBg: rgba($colorKey, 0.2);
$colorFutureFg: $colorCurrentFg;
$colorFutureBorder: $colorCurrentBorder;
// Tree
$colorTreeBg: transparent;
$colorItemTreeHoverBg: rgba(#fff, 0.1);

View File

@@ -349,7 +349,7 @@ $shdwItemText: none;
$colorTabBorder: pullForward($colorBodyBg, 10%);
$colorTabBodyBg: $colorBodyBg;
$colorTabBodyFg: pullForward($colorBodyFg, 20%);
$colorTabHeaderBg: #575757;
$colorTabHeaderBg: rgba($colorBodyFg, 0.15);
$colorTabHeaderFg: $colorBodyFg;
$colorTabHeaderBorder: $colorBodyBg;
$colorTabGroupHeaderBg: pullForward($colorBodyBg, 5%);
@@ -370,24 +370,6 @@ $legendHoverValueBg: rgba($colorBodyFg, 0.2);
$legendTableHeadBg: rgba($colorBodyFg, 0.15);
$colorPlotLimitLineBg: rgba($colorBodyBg, 0.2);
// Gauges
$colorGaugeBg: pullForward($colorBodyBg, 5%); // Gauge radial area background, meter background
$colorGaugeValue: rgba(#fff, 0.3); // Gauge value graphic (radial sweep, bar) color
$colorGaugeTextValue: #fff; // Radial gauge text value
$colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid on value bar
$colorGaugeRange: $colorBodyFg; // Range text color
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4);
$colorGaugeLimitLow: $colorGaugeLimitHigh;
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
// Time Strip and Lists
$colorCurrentBg: rgba($colorStatusAlert, 0.3);
$colorCurrentFg: pullForward($colorBodyFg, 20%);
$colorCurrentBorder: #fff;
$colorFutureBg: rgba($colorKey, 0.2);
$colorFutureFg: $colorCurrentFg;
$colorFutureBorder: $colorCurrentBorder;
// Tree
$colorTreeBg: transparent;
$colorItemTreeHoverBg: rgba(#fff, 0.03);

View File

@@ -345,7 +345,7 @@ $shdwItemText: none;
$colorTabBorder: pullForward($colorBodyBg, 10%);
$colorTabBodyBg: $colorBodyBg;
$colorTabBodyFg: pullForward($colorBodyFg, 20%);
$colorTabHeaderBg: #e2e2e2;
$colorTabHeaderBg: rgba($colorBodyFg, 0.2);
$colorTabHeaderFg: $colorBodyFg;
$colorTabHeaderBorder: $colorBodyBg;
$colorTabGroupHeaderBg: pullForward($colorBodyBg, 5%);
@@ -366,24 +366,6 @@ $legendHoverValueBg: rgba($colorBodyFg, 0.2);
$legendTableHeadBg: rgba($colorBodyFg, 0.15);
$colorPlotLimitLineBg: rgba($colorBodyBg, 0.4);
// Gauges
$colorGaugeBg: pullForward($colorBodyBg, 20%); // Gauge radial area background, meter background
$colorGaugeValue: rgba(#000, 0.3); // Gauge value graphic (radial sweep, bar) color
$colorGaugeTextValue: pullForward($colorBodyFg, 20%); // Radial gauge text value
$colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid on value bar
$colorGaugeRange: $colorBodyFg; // Range text color
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.2);
$colorGaugeLimitLow: $colorGaugeLimitHigh;
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
// Time Strip and Lists
$colorCurrentBg: rgba($colorStatusAlert, 0.3);
$colorCurrentFg: pullForward($colorBodyFg, 20%);
$colorCurrentBorder: #fff;
$colorFutureBg: rgba($colorKey, 0.2);
$colorFutureFg: $colorCurrentFg;
$colorFutureBorder: $colorCurrentBorder;
// Tree
$colorTreeBg: transparent;
$colorItemTreeHoverBg: rgba(black, 0.07);

View File

@@ -263,7 +263,6 @@ $glyph-icon-telemetry-aggregate: '\eb2b';
$glyph-icon-bar-chart: '\eb2c';
$glyph-icon-map: '\eb2d';
$glyph-icon-plan: '\eb2e';
$glyph-icon-timelist: '\eb2f';
/************************** GLYPHS AS DATA URI */
// Only objects have been converted, for use in Create menu and folder views
@@ -318,4 +317,3 @@ $bg-icon-condition-widget: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='h
$bg-icon-bar-chart: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M416 0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96ZM133.82 448H64V224h69.82Zm104.73 0h-69.82V64h69.82Zm104.72 0h-69.82V288h69.82ZM448 448h-69.82V128H448Z'/%3e%3c/svg%3e");
$bg-icon-map: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 32.7 384 64v448l64-31.3c35.2-17.21 64-60.1 64-95.3v-320c0-35.2-28.8-49.91-64-32.7ZM160 456l193.6 48.4v-448L160 8v448zM129.6.4 128 0 64 31.3C28.8 48.51 0 91.4 0 126.6v320c0 35.2 28.8 49.91 64 32.7l64-31.3 1.6.4Z'/%3e%3c/svg%3e");
$bg-icon-plan: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cg data-name='Layer 1'%3e%3cpath fill='%23000000' d='M128 96V64a64.19 64.19 0 0 1 64-64h128a64.19 64.19 0 0 1 64 64v32Z'/%3e%3cpath fill='%23000000' d='M416 64v64H96V64c-52.8 0-96 43.2-96 96v256c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V160c0-52.8-43.2-96-96-96ZM64 288v-64h128v64Zm256 128H128v-64h192Zm128 0h-64v-64h64Zm0-128H256v-64h192Z'/%3e%3c/g%3e%3c/g%3e%3c/svg%3e");
$bg-icon-timelist: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cpath d='M448 0H64A64.19 64.19 0 0 0 0 64v384a64.19 64.19 0 0 0 64 64h384a64.19 64.19 0 0 0 64-64V64a64.19 64.19 0 0 0-64-64ZM213.47 266.73a24 24 0 0 1-32.2 10.74L104 238.83V128a24 24 0 0 1 48 0v81.17l50.73 25.36a24 24 0 0 1 10.74 32.2ZM448 448H288v-64h160Zm0-96H288v-64h160Zm0-96H288v-64h160Zm0-96H288V96h160Z' data-name='Layer 1'/%3e%3c/g%3e%3c/svg%3e");

View File

@@ -70,24 +70,8 @@
padding: $formTBPad $formLRPad;
text-transform: uppercase;
}
&--sub-grid {
// 3 columns: <req> <label> <input/control>
display: grid;
grid-column-gap: $interiorMargin;
grid-template-columns: 20px max-content 1fr;
grid-row-gap: $interiorMargin;
margin-top: $interiorMarginLg;
width: max-content;
.c-form__row {
display: contents;
}
}
}
.c-form-row {
align-items: start;

View File

@@ -194,7 +194,6 @@
.icon-bar-chart { @include glyphBefore($glyph-icon-bar-chart); }
.icon-map { @include glyphBefore($glyph-icon-map); }
.icon-plan { @include glyphBefore($glyph-icon-plan); }
.icon-timelist { @include glyphBefore($glyph-icon-timelist); }
/************************** 12 PX CLASSES */
// TODO: sync with 16px redo as of 10/25/18
@@ -257,4 +256,3 @@
.bg-icon-bar-chart { @include glyphBg($bg-icon-bar-chart); }
.bg-icon-map { @include glyphBg($bg-icon-map); }
.bg-icon-plan { @include glyphBg($bg-icon-plan); }
.bg-icon-timelist { @include glyphBg($bg-icon-timelist); }

File diff suppressed because it is too large Load Diff

View File

@@ -162,5 +162,4 @@
<glyph unicode="&#xeb2c;" glyph-name="icon-bar-graph" d="M832 832h-640c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM267.64-64h-139.64v448h139.64zM477.1-64h-139.64v768h139.64zM686.54-64h-139.64v320h139.64zM896-64h-139.64v640h139.64z" />
<glyph unicode="&#xeb2d;" glyph-name="icon-map" d="M896 766.6l-128-62.6v-896l128 62.6c70.4 34.42 128 120.2 128 190.6v640c0 70.4-57.6 99.82-128 65.4zM320-80l387.2-96.8v896l-387.2 96.8v-896zM259.2 831.2l-3.2 0.8-128-62.6c-70.4-34.42-128-120.2-128-190.6v-640c0-70.4 57.6-99.82 128-65.4l128 62.6 3.2-0.8z" />
<glyph unicode="&#xeb2e;" glyph-name="icon-plan" d="M256 640v64c0.215 70.606 57.394 127.785 127.979 128h256.021c70.606-0.215 127.785-57.394 128-127.979v-64.021zM832 704v-128h-640v128c-105.6 0-192-86.4-192-192v-512c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v512c0 105.6-86.4 192-192 192zM128 256v128h256v-128zM640 0h-384v128h384zM896 0h-128v128h128zM896 256h-384v128h384z" />
<glyph unicode="&#xeb2f;" glyph-name="icon-timelist" d="M896 832h-768c-70.606-0.215-127.785-57.394-128-127.979v-768.021c0.215-70.606 57.394-127.785 127.979-128h768.021c70.606 0.215 127.785 57.394 128 127.979v768.021c-0.215 70.606-57.394 127.785-127.979 128h-0.021zM426.94 298.54c-8.054-15.864-24.249-26.545-42.938-26.545-7.823 0-15.209 1.871-21.734 5.191l0.273-0.126-154.54 77.28v221.66c0 26.51 21.49 48 48 48s48-21.49 48-48v0-162.34l101.46-50.72c15.864-8.054 26.545-24.249 26.545-42.938 0-7.823-1.871-15.209-5.191-21.734l0.126 0.273zM896-64h-320v128h320zM896 128h-320v128h320zM896 320h-320v128h320zM896 512h-320v128h320z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -15,6 +15,7 @@
@import "../plugins/flexibleLayout/components/flexible-layout.scss";
@import "../plugins/folderView/components/grid-view.scss";
@import "../plugins/folderView/components/list-item.scss";
@import "../plugins/folderView/components/list-view.scss";
@import "../plugins/imagery/components/imagery-view.scss";
@import "../plugins/imagery/components/Compass/compass.scss";
@import "../plugins/telemetryTable/components/table-row.scss";
@@ -27,7 +28,6 @@
@import "../plugins/timeConductor/conductor-mode-icon.scss";
@import "../plugins/timeConductor/date-picker.scss";
@import "../plugins/timeline/timeline.scss";
@import "../plugins/timelist/timelist.scss";
@import "../plugins/plan/plan";
@import "../plugins/viewDatumAction/components/metadata-list.scss";
@import "../ui/components/object-frame.scss";
@@ -37,7 +37,6 @@
@import "../ui/components/swim-lane/swimlane.scss";
@import "../ui/components/toggle-switch.scss";
@import "../ui/components/timesystem-axis.scss";
@import "../ui/components/List/list-view.scss";
@import "../ui/inspector/elements.scss";
@import "../ui/inspector/inspector.scss";
@import "../ui/inspector/location.scss";
@@ -53,7 +52,6 @@
@import "../ui/toolbar/components/toolbar-checkbox.scss";
@import "./notebook.scss";
@import "../plugins/notebook/components/sidebar.scss";
@import "../plugins/gauge/gauge.scss";
#splash-screen {
display: none;

View File

@@ -1,56 +0,0 @@
<template>
<th
v-if="isSortable"
class="is-sortable"
:class="{
'is-sorting': currentSort === property,
'asc': direction,
'desc': !direction
}"
@click="sort(property, direction)"
>
{{ title }}
</th>
<th v-else>
{{ title }}
</th>
</template>
<script>
export default {
inject: ['openmct'],
props: {
property: {
type: String,
required: true
},
currentSort: {
type: String,
required: true
},
title: {
type: String,
required: true
},
direction: {
type: Boolean,
required: true
},
isSortable: {
type: Boolean,
default() {
return false;
}
}
},
methods: {
sort(property, direction) {
this.$emit('sort', {
property,
direction
});
}
}
};
</script>

View File

@@ -1,53 +0,0 @@
<template>
<tr
class="c-list-item js-list-item"
:class="item.cssClass || ''"
>
<td
v-for="itemValue in formattedItemValues"
:key="itemValue.key"
class="c-list-item__value js-list-item__value"
:class="['--' + itemValue.key]"
:title="itemValue.text"
>
{{ itemValue.text }}
</td>
</tr>
</template>
<script>
import _ from 'lodash';
export default {
inject: ['openmct'],
props: {
item: {
type: Object,
required: true
},
itemProperties: {
type: Array,
required: true
}
},
computed: {
formattedItemValues() {
let values = [];
this.itemProperties.forEach((property) => {
// 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);
}
values.push({
text: value,
key: property.key
});
});
return values;
}
}
};
</script>

View File

@@ -1,142 +0,0 @@
<template>
<div class="c-table c-table--sortable c-list-view c-list-view--sticky-header">
<table class="c-table__body js-table__body">
<thead class="c-table__header">
<tr>
<list-header
v-for="headerItem in headerItems"
:key="headerItem.property"
:direction="sortBy === headerItem.property ? ascending : headerItem.defaultDirection"
:is-sortable="headerItem.isSortable"
:title="headerItem.name"
:property="headerItem.property"
:current-sort="sortBy"
@sort="sort"
/>
</tr>
</thead>
<tbody>
<list-item
v-for="item in sortedItems"
:key="item.key"
:item="item"
:item-properties="itemProperties"
/>
</tbody>
</table>
</div>
</template>
<script>
import ListItem from './ListItem.vue';
import ListHeader from './ListHeader.vue';
import _ from 'lodash';
export default {
components: {
ListItem,
ListHeader
},
inject: ['domainObject', 'openmct'],
props: {
headerItems: {
type: Array,
required: true
},
items: {
type: Array,
required: true
},
defaultSort: {
type: Object,
default() {
return {
property: '',
defaultDirection: true
};
}
},
storageKey: {
type: String,
default() {
return undefined;
}
}
},
data() {
let sortBy = this.defaultSort.property;
let ascending = this.defaultSort.defaultDirection;
if (this.storageKey) {
let persistedSortOrder = window.localStorage.getItem(this.storageKey);
if (persistedSortOrder) {
let parsed = JSON.parse(persistedSortOrder);
sortBy = parsed.sortBy;
ascending = parsed.ascending;
}
}
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 {
key: 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;
}
if (this.storageKey) {
window.localStorage
.setItem(
this.storageKey,
JSON.stringify(
{
sortBy: this.sortBy,
ascending: this.ascending
}
)
);
}
}
}
};
</script>

View File

@@ -1,40 +0,0 @@
/******************************* LIST VIEW */
.c-list-view {
tbody tr {
background: $colorListItemBg;
transition: $transOut;
}
td {
$p: floor($interiorMargin * 1.5);
@include ellipsize();
line-height: 120%; // Needed for icon alignment
max-width: 0;
padding-top: $p;
padding-bottom: $p;
width: 25%;
}
&--selectable {
body.desktop & {
tbody tr {
cursor: pointer;
&:hover {
background: $colorListItemBgHov;
filter: $filterHov;
transition: $transIn;
}
}
}
}
&--sticky-header {
thead tr {
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 2;
}
}
}

View File

@@ -213,6 +213,12 @@ export default {
}
}
});
if (this.domainObject && this.domainObject.type === 'conditionWidget' && keys.includes('output')) {
this.openmct.objects.mutate(this.domainObject, 'conditionalLabel', styleObj.output);
} else {
this.openmct.objects.mutate(this.domainObject, 'conditionalLabel', '');
}
},
updateView(immediatelySelect) {
this.clear();
@@ -444,3 +450,4 @@ export default {
}
};
</script>

View File

@@ -166,8 +166,7 @@ export default {
}
return definition.form
.filter(field => !field.hideFromInspector)
.map(field => {
.map((field) => {
let path = field.property;
if (typeof path === 'string') {
path = [path];

View File

@@ -79,11 +79,6 @@
padding-right: 0;
}
textarea {
// When a textarea is in the Inspector, only allow vertical resize
resize: vertical;
}
/************************************************************** LEGACY */
.l-inspector-part {
display: contents;
@@ -156,8 +151,7 @@
padding: 3px $interiorMarginLg 3px 0;
}
&__label,
&__hint {
&__label {
color: $colorInspectorPropName;
&[title]:not([title=""]) {
@@ -173,14 +167,6 @@
grid-column: 1 / 3;
}
}
.is-editing & {
.c-inspect-properties {
&__value, &__label {
line-height: 160%; // Prevent buttons/selects from overlapping when wrapping
}
}
}
}
/********************************************* INSPECTOR PROPERTIES TAB */

View File

@@ -22,10 +22,10 @@
<template>
<div class="l-preview-window js-preview-window">
<PreviewHeader
:current-view="currentViewProvider"
:current-view="currentView"
:action-collection="actionCollection"
:domain-object="domainObject"
:views="viewProviders"
:views="views"
/>
<div class="l-preview-window__object-view js-notebook-snapshot-item">
<div ref="objectView"></div>
@@ -52,12 +52,6 @@ export default {
default() {
return undefined;
}
},
existingView: {
type: Object,
default() {
return undefined;
}
}
},
data() {
@@ -66,25 +60,21 @@ export default {
return {
domainObject: domainObject,
viewKey: undefined,
viewProviders: [],
currentViewProvider: {},
actionCollection: undefined,
existingViewIndex: 0
views: [],
currentView: {},
actionCollection: undefined
};
},
mounted() {
this.viewProviders = this.openmct.objectViews.get(this.domainObject, this.objectPath);
this.viewProviders.forEach((provider, index) => {
provider.onItemClicked = () => {
if (this.existingView && provider.key === this.existingView.key) {
this.existingViewIndex = index;
}
this.setView(provider);
this.views = this.openmct.objectViews.get(this.domainObject, this.objectPath).map((view) => {
view.onItemClicked = () => {
return this.setView(view);
};
return view;
});
this.setView(this.viewProviders[0]);
this.setView(this.views[0]);
},
beforeDestroy() {
if (this.stopListeningStyles) {
@@ -101,74 +91,41 @@ export default {
}
},
destroyed() {
if (!this.existingView) {
this.view.destroy();
} else if (this.existingViewElement) {
// if the existing view element is populated, it's the currently viewed view
// in preview and we need to add it back to the parent.
this.addExistingViewBackToParent();
}
},
methods: {
clear() {
if (this.view) {
if (this.view !== this.existingView) {
this.view.destroy();
} else {
this.addExistingViewBackToParent();
this.$refs.objectView.innerHTML = '';
}
this.$refs.objectView.innerHTML = '';
delete this.view;
delete this.viewContainer;
}
},
setView(viewProvider) {
if (this.viewKey === viewProvider.key) {
setView(view) {
if (this.viewKey === view.key) {
return;
}
const isExistingView = viewProvider.key === this.existingView?.key;
this.clear();
this.viewKey = viewProvider.key;
this.initializeViewContainer();
if (isExistingView) {
this.view = this.existingView;
this.existingViewElement = this.existingView.parentElement.firstChild;
this.currentViewProvider = this.viewProviders[this.existingViewIndex];
} else {
this.currentViewProvider = viewProvider;
this.view = this.currentViewProvider.view(this.domainObject, this.objectPath);
}
this.getActionsCollection(this.view);
if (isExistingView) {
this.viewContainer.appendChild(this.existingViewElement);
} else {
this.view.show(this.viewContainer, false, this.viewOptions);
}
this.initObjectStyles();
},
addExistingViewBackToParent() {
this.existingView.parentElement.appendChild(this.existingViewElement);
delete this.existingViewElement;
},
initializeViewContainer() {
this.viewKey = view.key;
this.currentView = view;
this.viewContainer = document.createElement('div');
this.viewContainer.classList.add('l-angular-ov-wrapper');
this.$refs.objectView.append(this.viewContainer);
this.view = this.currentView.view(this.domainObject, this.objectPath);
this.getActionsCollection();
this.view.show(this.viewContainer, false, this.viewOptions);
this.initObjectStyles();
},
getActionsCollection(view) {
getActionsCollection() {
if (this.actionCollection) {
this.actionCollection.destroy();
}
this.actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, view);
this.actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.view);
},
initObjectStyles() {
if (!this.styleRuleManager) {

View File

@@ -90,7 +90,6 @@ define(['EventEmitter'], function (EventEmitter) {
provider.view = (domainObject, objectPath) => {
const viewObject = wrappedView(domainObject, objectPath);
const wrappedShow = viewObject.show.bind(viewObject);
viewObject.key = key; // provide access to provider key on view object
viewObject.show = (element, isEditing, viewOptions) => {
viewObject.parentElement = element.parentElement;
wrappedShow(element, isEditing, viewOptions);