Compare commits
15 Commits
log-plots-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21ae9f45c1 | ||
|
|
0a40c8dd0b | ||
|
|
ef1ea8e712 | ||
|
|
5c4fad77ff | ||
|
|
dbac9e6cd2 | ||
|
|
4b7bcf9c89 | ||
|
|
2b42abd495 | ||
|
|
1f2102b845 | ||
|
|
2ccb90aa41 | ||
|
|
525496fbca | ||
|
|
47099786cb | ||
|
|
3a11291a3b | ||
|
|
476f1b2579 | ||
|
|
6153ad8e1e | ||
|
|
77c0b16050 |
2
.github/workflows/e2e-pr.yml
vendored
2
.github/workflows/e2e-pr.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- run: npm install
|
||||
- run: npm run test:e2e:full
|
||||
- name: Archive test results
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
- name: Test success
|
||||
|
||||
190
e2e/tests/plugins/plot/autoscale.e2e.spec.js
Normal file
190
e2e/tests/plugins/plot/autoscale.e2e.spec.js
Normal file
@@ -0,0 +1,190 @@
|
||||
/*****************************************************************************
|
||||
* 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.only('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: 20 }))
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @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.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -1,279 +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.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify log plot functionality.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Log plot tests', () => {
|
||||
test.only('Can create a log plot.', async ({ page }) => {
|
||||
await makeOverlayPlot(page);
|
||||
await testRegularTicks(page);
|
||||
await enableEditMode(page);
|
||||
await enableLogMode(page);
|
||||
await testLogTicks(page);
|
||||
await disableLogMode(page);
|
||||
await testRegularTicks(page);
|
||||
await enableLogMode(page);
|
||||
await testLogTicks(page);
|
||||
await saveOverlayPlot(page);
|
||||
await testLogTicks(page);
|
||||
await testLogPlotPixels(page);
|
||||
|
||||
// refresh page
|
||||
await page.reload();
|
||||
|
||||
// test log ticks hold up after refresh
|
||||
await testLogTicks(page);
|
||||
await testLogPlotPixels(page);
|
||||
});
|
||||
|
||||
test.only('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
|
||||
await makeOverlayPlot(page);
|
||||
await enableEditMode(page);
|
||||
await enableLogMode(page);
|
||||
await saveOverlayPlot(page);
|
||||
|
||||
// TODO ...export, delete the overlay, then import it...
|
||||
|
||||
await testLogTicks(page);
|
||||
|
||||
// TODO, the plot is slightly at different position that in the other test, so this fails.
|
||||
// ...We can fix it by copying all steps from the first test...
|
||||
// await testLogPlotPixels(page);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Makes an overlay plot with a sine wave generator and clicks on the overlay plot in the sidebar so it is the active thing displayed.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function makeOverlayPlot(page) {
|
||||
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// 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('2022-03-29 22:00:00.000Z');
|
||||
|
||||
await timeInputs.nth(1).click();
|
||||
await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z');
|
||||
|
||||
// create overlay plot
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-overlay' }*/),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// save the overlay plot
|
||||
|
||||
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();
|
||||
|
||||
// create a sinewave generator
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
// set amplitude to 6, offset 4, period 2
|
||||
|
||||
await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
|
||||
await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').fill('6');
|
||||
|
||||
await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click();
|
||||
await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').fill('4');
|
||||
|
||||
await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click();
|
||||
await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('2');
|
||||
|
||||
// Click OK to make generator
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f/6e58b26a-8a73-4df6-b3a6-918decc0bbfa?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-single' }*/),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// click on 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/8caf7072-535b-4af6-8394-edd86e3ea35f?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-overlay' }*/),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testRegularTicks(page) {
|
||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
||||
expect(await yTicks.count()).toBe(7);
|
||||
await expect(yTicks.nth(0)).toHaveText('-2');
|
||||
await expect(yTicks.nth(1)).toHaveText('0');
|
||||
await expect(yTicks.nth(2)).toHaveText('2');
|
||||
await expect(yTicks.nth(3)).toHaveText('4');
|
||||
await expect(yTicks.nth(4)).toHaveText('6');
|
||||
await expect(yTicks.nth(5)).toHaveText('8');
|
||||
await expect(yTicks.nth(6)).toHaveText('10');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testLogTicks(page) {
|
||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
||||
expect(await yTicks.count()).toBe(28);
|
||||
await expect(yTicks.nth(0)).toHaveText('-2.98');
|
||||
await expect(yTicks.nth(1)).toHaveText('-2.50');
|
||||
await expect(yTicks.nth(2)).toHaveText('-2.00');
|
||||
await expect(yTicks.nth(3)).toHaveText('-1.51');
|
||||
await expect(yTicks.nth(4)).toHaveText('-1.20');
|
||||
await expect(yTicks.nth(5)).toHaveText('-1.00');
|
||||
await expect(yTicks.nth(6)).toHaveText('-0.80');
|
||||
await expect(yTicks.nth(7)).toHaveText('-0.58');
|
||||
await expect(yTicks.nth(8)).toHaveText('-0.40');
|
||||
await expect(yTicks.nth(9)).toHaveText('-0.20');
|
||||
await expect(yTicks.nth(10)).toHaveText('-0.00');
|
||||
await expect(yTicks.nth(11)).toHaveText('0.20');
|
||||
await expect(yTicks.nth(12)).toHaveText('0.40');
|
||||
await expect(yTicks.nth(13)).toHaveText('0.58');
|
||||
await expect(yTicks.nth(14)).toHaveText('0.80');
|
||||
await expect(yTicks.nth(15)).toHaveText('1.00');
|
||||
await expect(yTicks.nth(16)).toHaveText('1.20');
|
||||
await expect(yTicks.nth(17)).toHaveText('1.51');
|
||||
await expect(yTicks.nth(18)).toHaveText('2.00');
|
||||
await expect(yTicks.nth(19)).toHaveText('2.50');
|
||||
await expect(yTicks.nth(20)).toHaveText('2.98');
|
||||
await expect(yTicks.nth(21)).toHaveText('3.50');
|
||||
await expect(yTicks.nth(22)).toHaveText('4.00');
|
||||
await expect(yTicks.nth(23)).toHaveText('4.50');
|
||||
await expect(yTicks.nth(24)).toHaveText('5.31');
|
||||
await expect(yTicks.nth(25)).toHaveText('7.00');
|
||||
await expect(yTicks.nth(26)).toHaveText('8.00');
|
||||
await expect(yTicks.nth(27)).toHaveText('9.00');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enableEditMode(page) {
|
||||
// turn on edit mode
|
||||
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enableLogMode(page) {
|
||||
// turn on log mode
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function disableLogMode(page) {
|
||||
// turn off log mode
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function saveOverlayPlot(page) {
|
||||
// save overlay plot
|
||||
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 testLogPlotPixels(page) {
|
||||
const pixelsMatch = await page.evaluate(async () => {
|
||||
// TODO get canvas pixels at a few locations to make sure they're the correct color, to test that the plot comes out as expected.
|
||||
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
|
||||
// These are some pixels that should be blue points in the log plot.
|
||||
// If the plot changes shape to an unexpected shape, this will
|
||||
// likely fail, which is what we want.
|
||||
//
|
||||
// I found these pixels by pausing playwright in debug mode at this
|
||||
// point, and using similar code as below to output the pixel data, then
|
||||
// I logged those pixels here.
|
||||
const expectedBluePixels = [
|
||||
// TODO these pixel sets only work with the first test, but not the second test.
|
||||
|
||||
// [60, 35],
|
||||
// [121, 125],
|
||||
// [156, 377],
|
||||
// [264, 73],
|
||||
// [372, 186],
|
||||
// [576, 73],
|
||||
// [659, 439],
|
||||
// [675, 423]
|
||||
|
||||
[60, 35],
|
||||
[120, 125],
|
||||
[156, 375],
|
||||
[264, 73],
|
||||
[372, 185],
|
||||
[575, 72],
|
||||
[659, 437],
|
||||
[675, 421]
|
||||
];
|
||||
|
||||
// The first canvas in the DOM is the one that has the plot point
|
||||
// icons (canvas 2d), which is the one we are testing. The second
|
||||
// one in the DOM is the WebGL canvas with the line. (Why aren't
|
||||
// they both WebGL?)
|
||||
const canvas = document.querySelector('canvas');
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
for (const pixel of expectedBluePixels) {
|
||||
// XXX Possible optimization: call getImageData only once with
|
||||
// area including all pixels to be tested.
|
||||
const data = ctx.getImageData(pixel[0], pixel[1], 1, 1).data;
|
||||
|
||||
// #43b0ffff <-- openmct cyanish-blue with 100% opacity
|
||||
// if (data[0] !== 0x43 || data[1] !== 0xb0 || data[2] !== 0xff || data[3] !== 0xff) {
|
||||
if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 0) {
|
||||
// If any pixel is empty, it means we didn't hit a plot point.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(pixelsMatch).toBe(true);
|
||||
}
|
||||
@@ -195,6 +195,7 @@
|
||||
));
|
||||
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
|
||||
openmct.install(openmct.plugins.Timer());
|
||||
openmct.install(openmct.plugins.Timelist());
|
||||
openmct.start();
|
||||
</script>
|
||||
</html>
|
||||
|
||||
21
package.json
21
package.json
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.0.2-SNAPSHOT",
|
||||
"version": "2.0.2",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.16.3",
|
||||
"@braintree/sanitize-url": "6.0.0",
|
||||
"@percy/cli": "1.0.0-beta.76",
|
||||
"@percy/playwright": "1.0.1",
|
||||
"@percy/cli": "1.0.4",
|
||||
"@percy/playwright": "1.0.2",
|
||||
"@playwright/test": "1.19.2",
|
||||
"@types/eventemitter3": "^1.0.0",
|
||||
"@types/jasmine": "^4.0.1",
|
||||
@@ -18,13 +18,12 @@
|
||||
"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.11.0",
|
||||
"eslint": "8.13.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.8.0",
|
||||
"eslint-plugin-vue": "8.5.0",
|
||||
@@ -38,7 +37,7 @@
|
||||
"imports-loader": "0.8.0",
|
||||
"jasmine-core": "4.0.1",
|
||||
"jsdoc": "3.5.5",
|
||||
"karma": "6.3.15",
|
||||
"karma": "6.3.18",
|
||||
"karma-chrome-launcher": "3.1.1",
|
||||
"karma-cli": "2.0.0",
|
||||
"karma-coverage": "2.1.1",
|
||||
@@ -54,7 +53,7 @@
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.6.0",
|
||||
"moment": "2.29.1",
|
||||
"moment-duration-format": "2.2.2",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.34",
|
||||
"node-bourbon": "4.2.3",
|
||||
"painterro": "1.2.56",
|
||||
@@ -62,14 +61,14 @@
|
||||
"plotly.js-gl2d-dist": "2.5.0",
|
||||
"printj": "1.3.1",
|
||||
"request": "2.88.2",
|
||||
"resolve-url-loader": "4.0.0",
|
||||
"resolve-url-loader": "5.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.2.0",
|
||||
"vue-eslint-parser": "8.3.0",
|
||||
"vue-loader": "15.9.8",
|
||||
"vue-template-compiler": "2.6.14",
|
||||
"webpack": "5.68.0",
|
||||
@@ -95,7 +94,6 @@
|
||||
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
|
||||
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor",
|
||||
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
||||
"test:e2e:debug": "npm run test:e2e:local -- --debug",
|
||||
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js default",
|
||||
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
|
||||
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
|
||||
@@ -113,6 +111,9 @@
|
||||
"engines": {
|
||||
"node": ">=14.19.1"
|
||||
},
|
||||
"overrides": {
|
||||
"core-js": "3.21.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"Firefox ESR",
|
||||
"not IE 11",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="c-table c-table--sortable c-list-view">
|
||||
<div class="c-table c-table--sortable c-list-view c-list-view--sticky-header c-list-view--selectable">
|
||||
<table class="c-table__body">
|
||||
<thead class="c-table__header">
|
||||
<tr>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/******************************* 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%;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,16 @@ 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) {
|
||||
constructor(openmct, domainObject, objectPath, options) {
|
||||
this.openmct = openmct;
|
||||
this.domainObject = domainObject;
|
||||
this.objectPath = objectPath;
|
||||
this.options = options;
|
||||
this.component = undefined;
|
||||
}
|
||||
|
||||
@@ -27,6 +32,7 @@ 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() {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
import ImageryView from './ImageryView';
|
||||
|
||||
export default function ImageryViewProvider(openmct) {
|
||||
export default function ImageryViewProvider(openmct, options) {
|
||||
const type = 'example.imagery';
|
||||
|
||||
function hasImageTelemetry(domainObject) {
|
||||
@@ -43,7 +43,7 @@ export default function ImageryViewProvider(openmct) {
|
||||
return hasImageTelemetry(domainObject) && (!isChildOfTimeStrip || openmct.router.isNavigatedObject(objectPath));
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
return new ImageryView(openmct, domainObject, objectPath);
|
||||
return new ImageryView(openmct, domainObject, objectPath, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -132,6 +132,10 @@
|
||||
<!-- 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>
|
||||
@@ -235,7 +239,7 @@ export default {
|
||||
ImageControls
|
||||
},
|
||||
mixins: [imageryData],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
|
||||
props: {
|
||||
focusedImageTimestamp: {
|
||||
type: Number,
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
@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;
|
||||
@@ -123,12 +133,17 @@
|
||||
// New imagery
|
||||
$bgColor: $colorOk;
|
||||
color: $colorOkFg;
|
||||
background: rgba($bgColor, 0.5);
|
||||
@include flash($animName: flashImageAge, $iter: 2, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
|
||||
background-color: rgba($bgColor, 0.5);
|
||||
animation-name: fade-out;
|
||||
animation-timing-function: ease-in;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
|
||||
&__thumbs-wrapper {
|
||||
display: flex; // Uses row layout
|
||||
justify-content: flex-end;
|
||||
|
||||
&.is-autoscroll-off {
|
||||
background: $colorInteriorBorder;
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
import ImageryViewProvider from './ImageryViewProvider';
|
||||
import ImageryTimestripViewProvider from './ImageryTimestripViewProvider';
|
||||
|
||||
export default function () {
|
||||
export default function (options) {
|
||||
return function install(openmct) {
|
||||
openmct.objectViews.addProvider(new ImageryViewProvider(openmct));
|
||||
openmct.objectViews.addProvider(new ImageryViewProvider(openmct, options));
|
||||
openmct.objectViews.addProvider(new ImageryTimestripViewProvider(openmct));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
ref="searchResults"
|
||||
:domain-object="domainObject"
|
||||
:results="searchResults"
|
||||
@cancelEdit="cancelTransaction"
|
||||
@editingEntry="startTransaction"
|
||||
@changeSectionPage="changeSelectedSection"
|
||||
@updateEntries="updateEntries"
|
||||
/>
|
||||
@@ -140,6 +142,8 @@
|
||||
:selected-page="selectedPage"
|
||||
:selected-section="selectedSection"
|
||||
:read-only="false"
|
||||
@cancelEdit="cancelTransaction"
|
||||
@editingEntry="startTransaction"
|
||||
@deleteEntry="deleteEntry"
|
||||
@updateEntry="updateEntry"
|
||||
/>
|
||||
@@ -710,6 +714,8 @@ 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;
|
||||
@@ -746,6 +752,39 @@ 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
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)"
|
||||
@@ -284,11 +285,16 @@ 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
:read-only="true"
|
||||
:selected-page="result.page"
|
||||
:selected-section="result.section"
|
||||
@editingEntry="editingEntry"
|
||||
@cancelEdit="cancelEdit"
|
||||
@changeSectionPage="changeSectionPage"
|
||||
@updateEntries="updateEntries"
|
||||
/>
|
||||
@@ -63,6 +65,12 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editingEntry() {
|
||||
this.$emit('editingEntry');
|
||||
},
|
||||
cancelEdit() {
|
||||
this.$emit('cancelEdit');
|
||||
},
|
||||
changeSectionPage(data) {
|
||||
this.$emit('changeSectionPage', data);
|
||||
},
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
class="gl-plot-tick-wrapper"
|
||||
>
|
||||
<div
|
||||
v-for="(tick, i) in ticks"
|
||||
:key="i"
|
||||
v-for="tick in ticks"
|
||||
:key="tick.value"
|
||||
class="gl-plot-tick gl-plot-x-tick-label"
|
||||
:style="{
|
||||
left: (100 * (tick.value - min) / interval) + '%'
|
||||
@@ -46,8 +46,8 @@
|
||||
class="gl-plot-tick-wrapper"
|
||||
>
|
||||
<div
|
||||
v-for="(tick, i) in ticks"
|
||||
:key="i"
|
||||
v-for="tick in ticks"
|
||||
:key="tick.value"
|
||||
class="gl-plot-tick gl-plot-y-tick-label"
|
||||
:style="{ top: (100 * (max - tick.value) / interval) + '%' }"
|
||||
:title="tick.fullText || tick.text"
|
||||
@@ -59,8 +59,8 @@
|
||||
<!-- grid lines follow -->
|
||||
<template v-if="position === 'right'">
|
||||
<div
|
||||
v-for="(tick, i) in ticks"
|
||||
:key="i"
|
||||
v-for="tick in ticks"
|
||||
:key="tick.value"
|
||||
class="gl-plot-hash hash-v"
|
||||
:style="{
|
||||
right: (100 * (max - tick.value) / interval) + '%',
|
||||
@@ -71,8 +71,8 @@
|
||||
</template>
|
||||
<template v-if="position === 'bottom'">
|
||||
<div
|
||||
v-for="(tick, i) in ticks"
|
||||
:key="i"
|
||||
v-for="tick in ticks"
|
||||
:key="tick.value"
|
||||
class="gl-plot-hash hash-h"
|
||||
:style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }"
|
||||
>
|
||||
@@ -83,7 +83,7 @@
|
||||
|
||||
<script>
|
||||
import eventHelpers from "./lib/eventHelpers";
|
||||
import { ticks, getLogTicks, getFormattedTicks } from "./tickUtils";
|
||||
import { ticks, getFormattedTicks } from "./tickUtils";
|
||||
import configStore from "./configuration/ConfigStore";
|
||||
|
||||
export default {
|
||||
@@ -96,13 +96,6 @@ export default {
|
||||
},
|
||||
required: true
|
||||
},
|
||||
// Make it a prop, then later we can allow user to change it via UI input
|
||||
tickCount: {
|
||||
type: Number,
|
||||
default() {
|
||||
return 6;
|
||||
}
|
||||
},
|
||||
position: {
|
||||
required: true,
|
||||
type: String,
|
||||
@@ -125,6 +118,7 @@ export default {
|
||||
|
||||
this.axis = this.getAxisFromConfig();
|
||||
|
||||
this.tickCount = 4;
|
||||
this.tickUpdate = false;
|
||||
this.listenTo(this.axis, 'change:displayRange', this.updateTicks, this);
|
||||
this.listenTo(this.axis, 'change:format', this.updateTicks, this);
|
||||
@@ -190,12 +184,7 @@ export default {
|
||||
}, this);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
return ticks(range.min, range.max, number);
|
||||
},
|
||||
|
||||
updateTicksForceRegeneration() {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
import MCTChartSeriesElement from './MCTChartSeriesElement';
|
||||
|
||||
export default class MCTChartLineStepAfter extends MCTChartSeriesElement {
|
||||
removePoint(index) {
|
||||
removePoint(point, index, count) {
|
||||
if (index > 0 && index / 2 < this.count) {
|
||||
this.buffer[index + 1] = this.buffer[index - 1];
|
||||
}
|
||||
|
||||
@@ -85,10 +85,11 @@ export default class MCTChartSeriesElement {
|
||||
|
||||
this.removeSegments(removalPoint, vertexCount);
|
||||
|
||||
// TODO useless makePoint call?
|
||||
this.makePoint(point, series);
|
||||
this.removePoint(removalPoint);
|
||||
|
||||
this.removePoint(
|
||||
this.makePoint(point, series),
|
||||
removalPoint,
|
||||
vertexCount
|
||||
);
|
||||
this.count -= (vertexCount / 2);
|
||||
}
|
||||
|
||||
@@ -108,7 +109,11 @@ export default class MCTChartSeriesElement {
|
||||
const insertionPoint = this.startIndexForPointAtIndex(index);
|
||||
this.growIfNeeded(pointsRequired);
|
||||
this.makeInsertionPoint(insertionPoint, pointsRequired);
|
||||
this.addPoint(this.makePoint(point, series), insertionPoint);
|
||||
this.addPoint(
|
||||
this.makePoint(point, series),
|
||||
insertionPoint,
|
||||
pointsRequired
|
||||
);
|
||||
this.count += (pointsRequired / 2);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.TEMPLATE;
|
||||
div.innerHTML = this.canvasTemplate + this.canvasTemplate;
|
||||
const mainCanvas = div.querySelectorAll("canvas")[1];
|
||||
const overlayCanvas = div.querySelectorAll("canvas")[0];
|
||||
this.canvas.parentNode.replaceChild(mainCanvas, this.canvas);
|
||||
|
||||
@@ -71,7 +71,6 @@ export default class Model extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @param {ModelOptions<T, O>} options
|
||||
*/
|
||||
initialize(options) {
|
||||
|
||||
@@ -157,7 +157,8 @@ export default class PlotConfigurationModel extends Model {
|
||||
@typedef {{
|
||||
configuration: {
|
||||
series: import('./PlotSeries').PlotSeriesModelType[]
|
||||
}
|
||||
yAxis: import('./YAxisModel').YAxisModelType
|
||||
},
|
||||
}} SomeDomainObject_NeedsName
|
||||
*/
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import _ from 'lodash';
|
||||
import Model from "./Model";
|
||||
import { MARKER_SHAPES } from '../draw/MarkerShapes';
|
||||
import configStore from "../configuration/ConfigStore";
|
||||
import { symlog } from '../mathUtils';
|
||||
|
||||
/**
|
||||
* Plot series handle interpreting telemetry metadata for a single telemetry
|
||||
@@ -64,8 +63,6 @@ import { symlog } from '../mathUtils';
|
||||
* @extends {Model<PlotSeriesModelType, PlotSeriesModelOptions>}
|
||||
*/
|
||||
export default class PlotSeries extends Model {
|
||||
logMode = false;
|
||||
|
||||
/**
|
||||
@param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options
|
||||
*/
|
||||
@@ -73,8 +70,6 @@ export default class PlotSeries extends Model {
|
||||
|
||||
super(options);
|
||||
|
||||
this.logMode = options.collection.plot.model.yAxis.logMode;
|
||||
|
||||
this.listenTo(this, 'change:xKey', this.onXKeyChange, this);
|
||||
this.listenTo(this, 'change:yKey', this.onYKeyChange, this);
|
||||
this.persistedConfig = options.persistedConfig;
|
||||
@@ -234,7 +229,6 @@ export default class PlotSeries extends Model {
|
||||
this.getXVal = format.parse.bind(format);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update y formatter on change, default to stepAfter interpolation if
|
||||
* y range is an enumeration.
|
||||
@@ -257,12 +251,7 @@ export default class PlotSeries extends Model {
|
||||
return this.limitEvaluator.evaluate(datum, valueMetadata);
|
||||
}.bind(this);
|
||||
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 ? scale * symlog(y, 10) : y;
|
||||
};
|
||||
this.getYVal = format.parse.bind(format);
|
||||
}
|
||||
|
||||
formatX(point) {
|
||||
@@ -530,8 +519,7 @@ export default class PlotSeries extends Model {
|
||||
|
||||
/**
|
||||
* Update the series data with the given value.
|
||||
* This return type definition is totally wrong, only covers sinwave generator. It needs to be generic.
|
||||
* @return-example {Array<{
|
||||
* @returns {Array<{
|
||||
cos: number
|
||||
sin: number
|
||||
mctLimitState: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import { antisymlog, symlog } from '../mathUtils';
|
||||
import Model from './Model';
|
||||
|
||||
/**
|
||||
@@ -31,7 +30,7 @@ import Model from './Model';
|
||||
*
|
||||
* `autoscale`: boolean, whether or not to autoscale.
|
||||
* `autoscalePadding`: float, percent of padding to display in plots.
|
||||
* `displayRange`: the current display range for the axis.
|
||||
* `displayRange`: the current display range for the x Axis.
|
||||
* `format`: the formatter for the axis.
|
||||
* `frozen`: boolean, if true, displayRange will not be updated automatically.
|
||||
* Used to temporarily disable automatic updates during user interaction.
|
||||
@@ -54,7 +53,6 @@ export default class YAxisModel extends Model {
|
||||
this.listenTo(this, 'change:stats', this.calculateAutoscaleExtents, this);
|
||||
this.listenTo(this, 'change:autoscale', this.toggleAutoscale, this);
|
||||
this.listenTo(this, 'change:autoscalePadding', this.updatePadding, this);
|
||||
this.listenTo(this, 'change:logMode', this.onLogModeChange, this);
|
||||
this.listenTo(this, 'change:frozen', this.toggleFreeze, this);
|
||||
this.listenTo(this, 'change:range', this.updateDisplayRange, this);
|
||||
this.updateDisplayRange(this.get('range'));
|
||||
@@ -64,14 +62,14 @@ 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);
|
||||
}
|
||||
@@ -141,11 +139,11 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
resetStats() {
|
||||
this.unset('stats');
|
||||
this.seriesCollection.forEach(function (series) {
|
||||
this.seriesCollection.forEach(series => {
|
||||
if (series.has('stats')) {
|
||||
this.updateStats(series.get('stats'));
|
||||
}
|
||||
}, this);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param {import('./PlotSeries').default} series
|
||||
@@ -171,41 +169,27 @@ export default class YAxisModel extends Model {
|
||||
if (autoscale && this.has('stats')) {
|
||||
this.set('displayRange', this.applyPadding(this.get('stats')));
|
||||
} else {
|
||||
this.set('displayRange', this.get('range'));
|
||||
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.
|
||||
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.
|
||||
this.set('range', this.get('displayRange'));
|
||||
}
|
||||
}
|
||||
}
|
||||
/** @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 = scale * symlog(range.min, 10);
|
||||
range.max = scale * symlog(range.max, 10);
|
||||
} else {
|
||||
range.min = antisymlog(range.min / scale, 10);
|
||||
range.max = antisymlog(range.max / scale, 10);
|
||||
}
|
||||
|
||||
this.set('displayRange', range);
|
||||
|
||||
this.resetSeries();
|
||||
}
|
||||
resetSeries() {
|
||||
this.plot.series.forEach((plotSeries) => {
|
||||
plotSeries.logMode = this.get('logMode');
|
||||
plotSeries.reset(plotSeries.getSeriesData());
|
||||
});
|
||||
// Update the series collection labels and formatting
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}
|
||||
/**
|
||||
* Update yAxis format, values, and label from known series.
|
||||
* @param {import('./SeriesCollection').default} seriesCollection
|
||||
*/
|
||||
updateFromSeries(seriesCollection) {
|
||||
const plotModel = this.plot.get('domainObject');
|
||||
const label = plotModel.configuration?.yAxis?.label;
|
||||
const label = plotModel?.configuration?.yAxis?.label;
|
||||
const sampleSeries = seriesCollection.first();
|
||||
if (!sampleSeries) {
|
||||
if (!label) {
|
||||
@@ -218,29 +202,22 @@ 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 / scale, 10)));
|
||||
} else {
|
||||
this.set('format', (n) => yFormat.format(n));
|
||||
}
|
||||
|
||||
this.set('format', yFormat.format.bind(yFormat));
|
||||
this.set('values', yMetadata.values);
|
||||
if (!label) {
|
||||
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;
|
||||
}
|
||||
const labelName = seriesCollection
|
||||
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).name : ''))
|
||||
.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
return '';
|
||||
}, undefined);
|
||||
|
||||
if (labelName) {
|
||||
this.set('label', labelName);
|
||||
@@ -248,19 +225,19 @@ export default class YAxisModel extends Model {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
const labelUnits = seriesCollection
|
||||
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).units : ''))
|
||||
.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
return '';
|
||||
}, undefined);
|
||||
|
||||
if (labelUnits) {
|
||||
this.set('label', labelUnits);
|
||||
@@ -272,15 +249,17 @@ export default class YAxisModel extends Model {
|
||||
/**
|
||||
* @override
|
||||
* @param {import('./Model').ModelOptions<YAxisModelType, YAxisModelOptions>} options
|
||||
* @returns {YAxisModelType}
|
||||
* @returns {Partial<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'.
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -290,9 +269,8 @@ export default class YAxisModel extends Model {
|
||||
/**
|
||||
@typedef {import('./XAxisModel').AxisModelType & {
|
||||
autoscale: boolean
|
||||
logMode: boolean
|
||||
autoscalePadding: number
|
||||
stats: import('./XAxisModel').NumberRange
|
||||
stats?: import('./XAxisModel').NumberRange
|
||||
values: Array<TODO>
|
||||
}} YAxisModelType
|
||||
*/
|
||||
|
||||
@@ -45,22 +45,14 @@
|
||||
>Label</div>
|
||||
<div class="grid-cell value">{{ label ? label : "Not defined" }}</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Enable log mode."
|
||||
>Log mode</div>
|
||||
<div class="grid-cell value">
|
||||
{{ logMode ? "Enabled" : "Disabled" }}
|
||||
</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Automatically scale the Y axis to keep all values in view."
|
||||
>Auto scale</div>
|
||||
>Autoscale</div>
|
||||
<div class="grid-cell value">
|
||||
{{ autoscale ? "Enabled: " + autoscalePadding : "Disabled" }}
|
||||
{{ autoscale ? "Enabled: " : "Disabled" }}
|
||||
{{ autoscale ? autoscalePadding : "" }}
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
@@ -150,7 +142,6 @@ export default {
|
||||
config: {},
|
||||
label: '',
|
||||
autoscale: '',
|
||||
logMode: false,
|
||||
autoscalePadding: '',
|
||||
rangeMin: '',
|
||||
rangeMax: '',
|
||||
@@ -181,7 +172,6 @@ export default {
|
||||
initConfiguration() {
|
||||
this.label = this.config.yAxis.get('label');
|
||||
this.autoscale = this.config.yAxis.get('autoscale');
|
||||
this.logMode = this.config.yAxis.get('logMode');
|
||||
this.autoscalePadding = this.config.yAxis.get('autoscalePadding');
|
||||
const range = this.config.yAxis.get('range');
|
||||
if (range) {
|
||||
|
||||
@@ -14,22 +14,9 @@
|
||||
@change="updateForm('label')"
|
||||
></div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Enable log mode."
|
||||
>
|
||||
Log mode
|
||||
</div>
|
||||
<div class="grid-cell value">
|
||||
<!-- eslint-disable-next-line vue/html-self-closing -->
|
||||
<input
|
||||
v-model="logMode"
|
||||
type="checkbox"
|
||||
@change="updateForm('logMode')"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="l-inspector-part">
|
||||
<h2>Y Axis Scaling</h2>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
@@ -65,10 +52,10 @@
|
||||
class="l-inspector-part"
|
||||
>
|
||||
<div
|
||||
v-show="!autoscale && validation.range"
|
||||
v-show="!autoscale && validationErrors.range"
|
||||
class="grid-span-all form-error"
|
||||
>
|
||||
{{ validation.range }}
|
||||
{{ validationErrors.range }}
|
||||
</div>
|
||||
<li class="grid-row force-border">
|
||||
<div
|
||||
@@ -101,7 +88,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { objectPath, validate } from "./formUtil";
|
||||
import { objectPath } from "./formUtil";
|
||||
import _ from "lodash";
|
||||
|
||||
export default {
|
||||
@@ -118,11 +105,10 @@ export default {
|
||||
return {
|
||||
label: '',
|
||||
autoscale: '',
|
||||
logMode: false,
|
||||
autoscalePadding: '',
|
||||
rangeMin: '',
|
||||
rangeMax: '',
|
||||
validation: {}
|
||||
validationErrors: {}
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -131,23 +117,23 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
initialize: function () {
|
||||
this.fields = {
|
||||
label: {
|
||||
this.fields = [
|
||||
{
|
||||
modelProp: 'label',
|
||||
objectPath: 'configuration.yAxis.label'
|
||||
},
|
||||
autoscale: {
|
||||
{
|
||||
modelProp: 'autoscale',
|
||||
coerce: Boolean,
|
||||
objectPath: 'configuration.yAxis.autoscale'
|
||||
},
|
||||
autoscalePadding: {
|
||||
{
|
||||
modelProp: 'autoscalePadding',
|
||||
coerce: Number,
|
||||
objectPath: 'configuration.yAxis.autoscalePadding'
|
||||
},
|
||||
logMode: {
|
||||
coerce: Boolean,
|
||||
objectPath: 'configuration.yAxis.logMode'
|
||||
},
|
||||
range: {
|
||||
{
|
||||
modelProp: 'range',
|
||||
objectPath: 'configuration.yAxis.range',
|
||||
coerce: function coerceRange(range) {
|
||||
if (!range) {
|
||||
@@ -192,29 +178,17 @@ export default {
|
||||
if (Number(range.min) > Number(range.max)) {
|
||||
return 'Minimum must be less than Maximum.';
|
||||
}
|
||||
|
||||
if (model.get('autoscale')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
];
|
||||
},
|
||||
initFormValues() {
|
||||
this.label = this.yAxis.get('label');
|
||||
this.autoscale = this.yAxis.get('autoscale');
|
||||
this.logMode = this.yAxis.get('logMode');
|
||||
this.autoscalePadding = this.yAxis.get('autoscalePadding');
|
||||
const range = this.yAxis.get('range');
|
||||
if (!range) {
|
||||
this.rangeMin = undefined;
|
||||
this.rangeMax = undefined;
|
||||
} else {
|
||||
this.rangeMin = range.min;
|
||||
this.rangeMax = range.max;
|
||||
}
|
||||
const range = this.yAxis.get('range') ?? this.yAxis.get('displayRange');
|
||||
this.rangeMin = range?.min;
|
||||
this.rangeMax = range?.max;
|
||||
},
|
||||
updateForm(formKey) {
|
||||
let newVal;
|
||||
@@ -228,23 +202,20 @@ export default {
|
||||
}
|
||||
|
||||
let oldVal = this.yAxis.get(formKey);
|
||||
const formField = this.fields[formKey];
|
||||
|
||||
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;
|
||||
const formField = this.fields.find((field) => field.modelProp === formKey);
|
||||
|
||||
const validationError = formField.validate?.(newVal, this.yAxis);
|
||||
this.validationErrors[formKey] = validationError;
|
||||
if (validationError) {
|
||||
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?
|
||||
// TODO: Why do we mutate yAxis twice, once directly, once via objects.mutate? Or are they different objects?
|
||||
this.yAxis.set(formKey, newVal);
|
||||
if (path) {
|
||||
this.openmct.objects.mutate(
|
||||
|
||||
@@ -15,15 +15,5 @@ export function validate(value, model, validateFunc) {
|
||||
}
|
||||
|
||||
export function objectPath(path) {
|
||||
if (path) {
|
||||
if (typeof path !== "function") {
|
||||
const staticObjectPath = path;
|
||||
|
||||
return function (object, model) {
|
||||
return staticObjectPath;
|
||||
};
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
return path && typeof path !== 'function' ? () => path : path;
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/** The natural number `e`. */
|
||||
export const e = Math.exp(1);
|
||||
|
||||
/**
|
||||
Returns the logarithm of a number, using the given base or the natural number
|
||||
`e` as base if not specified.
|
||||
@param {number} n
|
||||
@param {number=} base log base, defaults to e
|
||||
*/
|
||||
export function log(n, base = e) {
|
||||
if (base === e) {
|
||||
return Math.log(n);
|
||||
}
|
||||
|
||||
return Math.log(n) / Math.log(base);
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the inverse of the logarithm of a number, using the given base or the
|
||||
natural number `e` as base if not specified.
|
||||
@param {number} n
|
||||
@param {number=} base log base, defaults to e
|
||||
*/
|
||||
export function antilog(n, base = e) {
|
||||
return Math.pow(base, n);
|
||||
}
|
||||
|
||||
/**
|
||||
A symmetric logarithm function. See https://github.com/nasa/openmct/issues/2297#issuecomment-1032914258
|
||||
@param {number} n
|
||||
@param {number=} base log base, defaults to e
|
||||
*/
|
||||
export function symlog(n, base = e) {
|
||||
return Math.sign(n) * log(Math.abs(n) + 1, base);
|
||||
}
|
||||
|
||||
/**
|
||||
An inverse symmetric logarithm function. See https://github.com/nasa/openmct/issues/2297#issuecomment-1032914258
|
||||
@param {number} n
|
||||
@param {number=} base log base, defaults to e
|
||||
*/
|
||||
export function antisymlog(n, base = e) {
|
||||
return Math.sign(n) * (antilog(Math.abs(n), base) - 1);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { antisymlog, symlog } from "./mathUtils";
|
||||
|
||||
const e10 = Math.sqrt(50);
|
||||
const e5 = Math.sqrt(10);
|
||||
const e2 = Math.sqrt(2);
|
||||
@@ -42,50 +40,6 @@ function getPrecision(step) {
|
||||
return precision;
|
||||
}
|
||||
|
||||
export function getLogTicks(start, stop, mainTickCount = 8, secondaryTickCount = 6) {
|
||||
// log()'ed values
|
||||
const mainLogTicks = ticks(start, stop, mainTickCount);
|
||||
|
||||
// original values
|
||||
const scale = 1; // TODO get from UI, positive number above 0
|
||||
const mainTicks = mainLogTicks.map(n => antisymlog(n / scale, 10));
|
||||
|
||||
const result = [];
|
||||
|
||||
let i = 0;
|
||||
for (const logTick of mainLogTicks) {
|
||||
result.push(logTick);
|
||||
|
||||
if (i === mainLogTicks.length - 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
const tick = mainTicks[i];
|
||||
const nextTick = mainTicks[i + 1];
|
||||
const rangeBetweenMainTicks = nextTick - tick;
|
||||
|
||||
const secondaryLogTicks = ticks(
|
||||
tick + rangeBetweenMainTicks / (secondaryTickCount + 1),
|
||||
nextTick - rangeBetweenMainTicks / (secondaryTickCount + 1),
|
||||
secondaryTickCount - 2
|
||||
)
|
||||
.map(n => scale * symlog(n, 10));
|
||||
|
||||
result.push(...secondaryLogTicks);
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -76,7 +76,8 @@ define([
|
||||
'./timer/plugin',
|
||||
'./userIndicator/plugin',
|
||||
'../../example/exampleUser/plugin',
|
||||
'./localStorage/plugin'
|
||||
'./localStorage/plugin',
|
||||
'./timelist/plugin'
|
||||
], function (
|
||||
_,
|
||||
UTCTimeSystem,
|
||||
@@ -133,7 +134,8 @@ define([
|
||||
Timer,
|
||||
UserIndicator,
|
||||
ExampleUser,
|
||||
LocalStorage
|
||||
LocalStorage,
|
||||
TimeList
|
||||
) {
|
||||
const plugins = {};
|
||||
|
||||
@@ -210,6 +212,7 @@ define([
|
||||
plugins.DeviceClassifier = DeviceClassifier.default;
|
||||
plugins.UserIndicator = UserIndicator.default;
|
||||
plugins.LocalStorage = LocalStorage.default;
|
||||
plugins.Timelist = TimeList.default;
|
||||
|
||||
return plugins;
|
||||
});
|
||||
|
||||
@@ -9,7 +9,11 @@ export default class TelemetryTableView {
|
||||
this.objectPath = objectPath;
|
||||
this.component = undefined;
|
||||
|
||||
this.table = new TelemetryTable(domainObject, openmct);
|
||||
Object.defineProperty(this, 'table', {
|
||||
value: new TelemetryTable(domainObject, openmct),
|
||||
enumerable: false,
|
||||
configurable: false
|
||||
});
|
||||
}
|
||||
|
||||
getViewContext() {
|
||||
|
||||
394
src/plugins/timelist/Timelist.vue
Normal file
394
src/plugins/timelist/Timelist.vue
Normal file
@@ -0,0 +1,394 @@
|
||||
<!--
|
||||
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>
|
||||
67
src/plugins/timelist/TimelistViewProvider.js
Normal file
67
src/plugins/timelist/TimelistViewProvider.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/*****************************************************************************
|
||||
* 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
24
src/plugins/timelist/constants.js
Normal file
24
src/plugins/timelist/constants.js
Normal file
@@ -0,0 +1,24 @@
|
||||
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';
|
||||
124
src/plugins/timelist/inspector/EventProperties.vue
Normal file
124
src/plugins/timelist/inspector/EventProperties.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<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>
|
||||
91
src/plugins/timelist/inspector/Filtering.vue
Normal file
91
src/plugins/timelist/inspector/Filtering.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<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>
|
||||
@@ -0,0 +1,70 @@
|
||||
/*****************************************************************************
|
||||
* 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
146
src/plugins/timelist/inspector/TimelistPropertiesView.vue
Normal file
146
src/plugins/timelist/inspector/TimelistPropertiesView.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<!--
|
||||
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>
|
||||
68
src/plugins/timelist/plugin.js
Normal file
68
src/plugins/timelist/plugin.js
Normal file
@@ -0,0 +1,68 @@
|
||||
/*****************************************************************************
|
||||
* 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));
|
||||
|
||||
};
|
||||
}
|
||||
181
src/plugins/timelist/pluginSpec.js
Normal file
181
src/plugins/timelist/pluginSpec.js
Normal file
@@ -0,0 +1,181 @@
|
||||
/*****************************************************************************
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
55
src/plugins/timelist/timelist.scss
Normal file
55
src/plugins/timelist/timelist.scss
Normal file
@@ -0,0 +1,55 @@
|
||||
/*****************************************************************************
|
||||
* 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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -38,41 +38,41 @@ export default class ViewLargeAction {
|
||||
}
|
||||
|
||||
invoke(objectPath, view) {
|
||||
const parentElement = view.parentElement;
|
||||
let childElement = parentElement && parentElement.firstChild;
|
||||
performance.mark('viewlarge.start');
|
||||
const childElement = view?.parentElement?.firstChild;
|
||||
if (!childElement) {
|
||||
const message = "ViewLargeAction: missing element";
|
||||
this.openmct.notifications.error(message);
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
this._expand(objectPath, childElement);
|
||||
this._expand(objectPath, view);
|
||||
}
|
||||
|
||||
appliesTo(objectPath, view = {}) {
|
||||
const parentElement = view.parentElement;
|
||||
const element = parentElement && parentElement.firstChild;
|
||||
const viewLargeAction = element && !element.classList.contains('js-main-container')
|
||||
appliesTo(objectPath, view) {
|
||||
const childElement = view?.parentElement?.firstChild;
|
||||
|
||||
return childElement && !childElement.classList.contains('js-main-container')
|
||||
&& !this.openmct.router.isNavigatedObject(objectPath);
|
||||
|
||||
return viewLargeAction;
|
||||
}
|
||||
|
||||
_expand(objectPath, childElement) {
|
||||
const parentElement = childElement.parentElement;
|
||||
_expand(objectPath, view) {
|
||||
const element = this._getPreview(objectPath, view);
|
||||
|
||||
this.overlay = this.openmct.overlays.overlay({
|
||||
element: this._getPreview(objectPath),
|
||||
element,
|
||||
size: 'large',
|
||||
autoHide: false,
|
||||
onDestroy() {
|
||||
parentElement.append(childElement);
|
||||
onDestroy: () => {
|
||||
this.preview.$destroy();
|
||||
this.preview = undefined;
|
||||
delete this.preview;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_getPreview(objectPath) {
|
||||
const preview = new Vue({
|
||||
_getPreview(objectPath, view) {
|
||||
this.preview = new Vue({
|
||||
components: {
|
||||
Preview
|
||||
},
|
||||
@@ -80,9 +80,14 @@ export default class ViewLargeAction {
|
||||
openmct: this.openmct,
|
||||
objectPath
|
||||
},
|
||||
template: '<Preview></Preview>'
|
||||
data() {
|
||||
return {
|
||||
view
|
||||
};
|
||||
},
|
||||
template: '<Preview :existing-view="view"></Preview>'
|
||||
});
|
||||
|
||||
return preview.$mount().$el;
|
||||
return this.preview.$mount().$el;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ $shdwItemText: none;
|
||||
$colorTabBorder: pullForward($colorBodyBg, 10%);
|
||||
$colorTabBodyBg: $colorBodyBg;
|
||||
$colorTabBodyFg: pullForward($colorBodyFg, 20%);
|
||||
$colorTabHeaderBg: rgba($colorBodyFg, 0.15);
|
||||
$colorTabHeaderBg: #575757;
|
||||
$colorTabHeaderFg: $colorBodyFg;
|
||||
$colorTabHeaderBorder: $colorBodyBg;
|
||||
$colorTabGroupHeaderBg: pullForward($colorBodyBg, 5%);
|
||||
@@ -366,6 +366,14 @@ $legendHoverValueBg: rgba($colorBodyFg, 0.2);
|
||||
$legendTableHeadBg: $colorTabHeaderBg;
|
||||
$colorPlotLimitLineBg: rgba($colorBodyBg, 0.2);
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -349,7 +349,7 @@ $shdwItemText: none;
|
||||
$colorTabBorder: pullForward($colorBodyBg, 10%);
|
||||
$colorTabBodyBg: $colorBodyBg;
|
||||
$colorTabBodyFg: pullForward($colorBodyFg, 20%);
|
||||
$colorTabHeaderBg: rgba($colorBodyFg, 0.15);
|
||||
$colorTabHeaderBg: #575757;
|
||||
$colorTabHeaderFg: $colorBodyFg;
|
||||
$colorTabHeaderBorder: $colorBodyBg;
|
||||
$colorTabGroupHeaderBg: pullForward($colorBodyBg, 5%);
|
||||
@@ -370,6 +370,14 @@ $legendHoverValueBg: rgba($colorBodyFg, 0.2);
|
||||
$legendTableHeadBg: rgba($colorBodyFg, 0.15);
|
||||
$colorPlotLimitLineBg: rgba($colorBodyBg, 0.2);
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -345,7 +345,7 @@ $shdwItemText: none;
|
||||
$colorTabBorder: pullForward($colorBodyBg, 10%);
|
||||
$colorTabBodyBg: $colorBodyBg;
|
||||
$colorTabBodyFg: pullForward($colorBodyFg, 20%);
|
||||
$colorTabHeaderBg: rgba($colorBodyFg, 0.2);
|
||||
$colorTabHeaderBg: #e2e2e2;
|
||||
$colorTabHeaderFg: $colorBodyFg;
|
||||
$colorTabHeaderBorder: $colorBodyBg;
|
||||
$colorTabGroupHeaderBg: pullForward($colorBodyBg, 5%);
|
||||
@@ -366,6 +366,14 @@ $legendHoverValueBg: rgba($colorBodyFg, 0.2);
|
||||
$legendTableHeadBg: rgba($colorBodyFg, 0.15);
|
||||
$colorPlotLimitLineBg: rgba($colorBodyBg, 0.4);
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -263,6 +263,7 @@ $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
|
||||
@@ -317,3 +318,4 @@ $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");
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
.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
|
||||
@@ -256,3 +257,4 @@
|
||||
.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
@@ -162,4 +162,5 @@
|
||||
<glyph unicode="" 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="" 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="" 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="" 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: 62 KiB After Width: | Height: | Size: 63 KiB |
Binary file not shown.
Binary file not shown.
@@ -15,7 +15,6 @@
|
||||
@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";
|
||||
@@ -28,6 +27,7 @@
|
||||
@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,6 +37,7 @@
|
||||
@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";
|
||||
|
||||
56
src/ui/components/List/ListHeader.vue
Normal file
56
src/ui/components/List/ListHeader.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<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>
|
||||
53
src/ui/components/List/ListItem.vue
Normal file
53
src/ui/components/List/ListItem.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<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>
|
||||
142
src/ui/components/List/ListView.vue
Normal file
142
src/ui/components/List/ListView.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<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>
|
||||
40
src/ui/components/List/list-view.scss
Normal file
40
src/ui/components/List/list-view.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
/******************************* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,11 @@
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
// When a textarea is in the Inspector, only allow vertical resize
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/************************************************************** LEGACY */
|
||||
.l-inspector-part {
|
||||
display: contents;
|
||||
@@ -151,7 +156,8 @@
|
||||
padding: 3px $interiorMarginLg 3px 0;
|
||||
}
|
||||
|
||||
&__label {
|
||||
&__label,
|
||||
&__hint {
|
||||
color: $colorInspectorPropName;
|
||||
|
||||
&[title]:not([title=""]) {
|
||||
@@ -167,6 +173,14 @@
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
.is-editing & {
|
||||
.c-inspect-properties {
|
||||
&__value, &__label {
|
||||
line-height: 160%; // Prevent buttons/selects from overlapping when wrapping
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/********************************************* INSPECTOR PROPERTIES TAB */
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
<template>
|
||||
<div class="l-preview-window js-preview-window">
|
||||
<PreviewHeader
|
||||
:current-view="currentView"
|
||||
:current-view="currentViewProvider"
|
||||
:action-collection="actionCollection"
|
||||
:domain-object="domainObject"
|
||||
:views="views"
|
||||
:views="viewProviders"
|
||||
/>
|
||||
<div class="l-preview-window__object-view js-notebook-snapshot-item">
|
||||
<div ref="objectView"></div>
|
||||
@@ -52,6 +52,12 @@ export default {
|
||||
default() {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
existingView: {
|
||||
type: Object,
|
||||
default() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -60,21 +66,25 @@ export default {
|
||||
return {
|
||||
domainObject: domainObject,
|
||||
viewKey: undefined,
|
||||
views: [],
|
||||
currentView: {},
|
||||
actionCollection: undefined
|
||||
viewProviders: [],
|
||||
currentViewProvider: {},
|
||||
actionCollection: undefined,
|
||||
existingViewIndex: 0
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.views = this.openmct.objectViews.get(this.domainObject, this.objectPath).map((view) => {
|
||||
view.onItemClicked = () => {
|
||||
return this.setView(view);
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
return view;
|
||||
this.setView(provider);
|
||||
};
|
||||
});
|
||||
|
||||
this.setView(this.views[0]);
|
||||
this.setView(this.viewProviders[0]);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.stopListeningStyles) {
|
||||
@@ -91,41 +101,74 @@ export default {
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.view.destroy();
|
||||
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) {
|
||||
this.view.destroy();
|
||||
this.$refs.objectView.innerHTML = '';
|
||||
}
|
||||
if (this.view !== this.existingView) {
|
||||
this.view.destroy();
|
||||
} else {
|
||||
this.addExistingViewBackToParent();
|
||||
}
|
||||
|
||||
delete this.view;
|
||||
delete this.viewContainer;
|
||||
this.$refs.objectView.innerHTML = '';
|
||||
delete this.view;
|
||||
delete this.viewContainer;
|
||||
}
|
||||
},
|
||||
setView(view) {
|
||||
if (this.viewKey === view.key) {
|
||||
setView(viewProvider) {
|
||||
if (this.viewKey === viewProvider.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isExistingView = viewProvider.key === this.existingView.key;
|
||||
|
||||
this.clear();
|
||||
this.viewKey = view.key;
|
||||
this.currentView = view;
|
||||
|
||||
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.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() {
|
||||
getActionsCollection(view) {
|
||||
if (this.actionCollection) {
|
||||
this.actionCollection.destroy();
|
||||
}
|
||||
|
||||
this.actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.view);
|
||||
this.actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, view);
|
||||
},
|
||||
initObjectStyles() {
|
||||
if (!this.styleRuleManager) {
|
||||
|
||||
@@ -90,6 +90,7 @@ 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);
|
||||
|
||||
Reference in New Issue
Block a user