mirror of
https://github.com/nasa/openmct.git
synced 2025-05-31 06:31:04 +00:00
Merge branch 'master' of https://github.com/nasa/openmct into conditionset-errors
This commit is contained in:
commit
84d8d8b0e8
12
.github/workflows/e2e-couchdb.yml
vendored
12
.github/workflows/e2e-couchdb.yml
vendored
@ -51,12 +51,12 @@ jobs:
|
||||
env:
|
||||
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }}
|
||||
run: npm run test:e2e:couchdb
|
||||
|
||||
|
||||
- name: Generate Code Coverage Report
|
||||
run: npm run cov:e2e:report
|
||||
|
||||
- name: Publish Results to Codecov.io
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage/e2e/lcov.info
|
||||
@ -66,15 +66,19 @@ jobs:
|
||||
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-couchdb-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Archive html test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-couchdb-html-test-results
|
||||
path: html-test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e:couchdb label (if present)
|
||||
if: always()
|
||||
|
4
.github/workflows/e2e-flakefinder.yml
vendored
4
.github/workflows/e2e-flakefinder.yml
vendored
@ -38,9 +38,11 @@ jobs:
|
||||
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-flakefinder-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e:flakefinder label (if present)
|
||||
if: always()
|
||||
|
4
.github/workflows/e2e-perf.yml
vendored
4
.github/workflows/e2e-perf.yml
vendored
@ -35,9 +35,11 @@ jobs:
|
||||
- run: npm run test:perf:memory
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-perf-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e:perf label (if present)
|
||||
if: always()
|
||||
|
4
.github/workflows/e2e-pr.yml
vendored
4
.github/workflows/e2e-pr.yml
vendored
@ -45,9 +45,11 @@ jobs:
|
||||
npm run cov:e2e:full:publish
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-pr-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e label (if present)
|
||||
if: always()
|
||||
|
@ -1,6 +1,10 @@
|
||||
codecov:
|
||||
require_ci_to_pass: false #This setting will update the bot regardless of whether or not tests pass
|
||||
|
||||
# Disabling annotations for now. They are incorrectly labelling lines as lacking coverage when they are in fact covered by tests.
|
||||
github_checks:
|
||||
annotations: false
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
|
@ -2,7 +2,6 @@
|
||||
module.exports = {
|
||||
extends: ['plugin:playwright/recommended'],
|
||||
rules: {
|
||||
'playwright/max-nested-describe': ['error', { max: 1 }],
|
||||
'playwright/expect-expect': 'off'
|
||||
},
|
||||
overrides: [
|
||||
|
@ -103,25 +103,40 @@ const extendedTest = test.extend({
|
||||
* Default: `true`
|
||||
*/
|
||||
failOnConsoleError: [true, { option: true }],
|
||||
ignore404s: [[], { option: true }],
|
||||
/**
|
||||
* Extends the base page class to enable console log error detection.
|
||||
* @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}
|
||||
*/
|
||||
page: async ({ page, failOnConsoleError }, use) => {
|
||||
page: async ({ page, failOnConsoleError, ignore404s }, use) => {
|
||||
// Capture any console errors during test execution
|
||||
const messages = [];
|
||||
let messages = [];
|
||||
page.on('console', (msg) => messages.push(msg));
|
||||
|
||||
await use(page);
|
||||
|
||||
if (ignore404s.length > 0) {
|
||||
messages = messages.filter((msg) => {
|
||||
let keep = true;
|
||||
|
||||
if (msg.text().match(/404 \((Object )?Not Found\)/) !== null) {
|
||||
keep = ignore404s.every((ignoreRule) => {
|
||||
return msg.location().url.match(ignoreRule) === null;
|
||||
});
|
||||
}
|
||||
|
||||
return keep;
|
||||
});
|
||||
}
|
||||
|
||||
// Assert against console errors during teardown
|
||||
if (failOnConsoleError) {
|
||||
messages.forEach((msg) =>
|
||||
messages.forEach((msg) => {
|
||||
// eslint-disable-next-line playwright/no-standalone-expect
|
||||
expect
|
||||
.soft(msg.type(), `Console error detected: ${_consoleMessageToString(msg)}`)
|
||||
.not.toEqual('error')
|
||||
);
|
||||
.not.toEqual('error');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -54,8 +54,7 @@ const examplePlanSmall1 = JSON.parse(
|
||||
const TIME_TO_FROM_COLUMN = 2;
|
||||
const HEADER_ROW = 0;
|
||||
const NUM_COLUMNS = 5;
|
||||
const FULL_CIRCLE_PATH =
|
||||
'M3.061616997868383e-15,-50A50,50,0,1,1,-3.061616997868383e-15,50A50,50,0,1,1,3.061616997868383e-15,-50Z';
|
||||
const FULL_CIRCLE_PATH = 'M0,-50A50,50,0,1,1,0,50A50,50,0,1,1,0,-50Z';
|
||||
|
||||
/**
|
||||
* The regular expression used to parse the countdown string.
|
||||
|
@ -24,7 +24,9 @@ import {
|
||||
createDomainObjectWithDefaults,
|
||||
createPlanFromJSON,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
setFixedIndependentTimeConductorBounds
|
||||
setFixedIndependentTimeConductorBounds,
|
||||
setFixedTimeMode,
|
||||
setTimeConductorBounds
|
||||
} from '../../../appActions.js';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
@ -74,21 +76,14 @@ const testPlan = {
|
||||
};
|
||||
|
||||
test.describe('Time Strip', () => {
|
||||
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5627'
|
||||
});
|
||||
|
||||
// Constant locators
|
||||
const activityBounds = page.locator('.activity-bounds');
|
||||
let timestrip;
|
||||
let plan;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Goto baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const timestrip = await test.step('Create a Time Strip', async () => {
|
||||
timestrip = await test.step('Create a Time Strip', async () => {
|
||||
const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
|
||||
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
|
||||
expect(objectName).toBe(createdTimeStrip.name);
|
||||
@ -96,7 +91,7 @@ test.describe('Time Strip', () => {
|
||||
return createdTimeStrip;
|
||||
});
|
||||
|
||||
const plan = await test.step('Create a Plan and add it to the timestrip', async () => {
|
||||
plan = await test.step('Create a Plan and add it to the timestrip', async () => {
|
||||
const createdPlan = await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: testPlan
|
||||
@ -110,6 +105,22 @@ test.describe('Time Strip', () => {
|
||||
.dragTo(page.getByLabel('Object View'));
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
return createdPlan;
|
||||
});
|
||||
});
|
||||
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5627'
|
||||
});
|
||||
|
||||
// Constant locators
|
||||
const activityBounds = page.locator('.activity-bounds');
|
||||
|
||||
await test.step('Set time strip to fixed timespan mode and verify activities', async () => {
|
||||
const startBound = testPlan.TEST_GROUP[0].start;
|
||||
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
||||
|
||||
@ -119,8 +130,6 @@ test.describe('Time Strip', () => {
|
||||
// Verify all events are displayed
|
||||
const eventCount = await page.locator('.activity-bounds').count();
|
||||
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
|
||||
|
||||
return createdPlan;
|
||||
});
|
||||
|
||||
await test.step('TimeStrip can use the Independent Time Conductor', async () => {
|
||||
@ -177,4 +186,48 @@ test.describe('Time Strip', () => {
|
||||
expect(await activityBounds.count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Time strip now line', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7817'
|
||||
});
|
||||
|
||||
await test.step('Is displayed in realtime mode', async () => {
|
||||
await expect(page.getByLabel('Now Marker')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Is hidden when out of bounds of the time axis', async () => {
|
||||
// Switch to fixed timespan mode
|
||||
await setFixedTimeMode(page);
|
||||
// Get the end bounds
|
||||
const endBounds = await page.getByLabel('End bounds').textContent();
|
||||
|
||||
// Add 2 minutes to end bound datetime and use it as the new end time
|
||||
let endTimeStamp = new Date(endBounds);
|
||||
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() + 2);
|
||||
const endDate = endTimeStamp.toISOString().split('T')[0];
|
||||
const milliseconds = endTimeStamp.getMilliseconds();
|
||||
const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');
|
||||
|
||||
// Subtract 1 minute from the end bound and use it as the new start time
|
||||
let startTimeStamp = new Date(endBounds);
|
||||
startTimeStamp.setUTCMinutes(startTimeStamp.getUTCMinutes() + 1);
|
||||
const startDate = startTimeStamp.toISOString().split('T')[0];
|
||||
const startMilliseconds = startTimeStamp.getMilliseconds();
|
||||
const startTime = startTimeStamp
|
||||
.toISOString()
|
||||
.split('T')[1]
|
||||
.replace(`.${startMilliseconds}Z`, '');
|
||||
// Set fixed timespan mode to the future so that "now" is out of bounds.
|
||||
await setTimeConductorBounds(page, {
|
||||
startDate,
|
||||
endDate,
|
||||
startTime,
|
||||
endTime
|
||||
});
|
||||
|
||||
await expect(page.getByLabel('Now Marker')).toBeHidden();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -108,4 +108,42 @@ test.describe('Plot Controls', () => {
|
||||
// Expect before and after plot points to match
|
||||
await expect(plotPixelSizeAtPause).toEqual(plotPixelSizeAfterWait);
|
||||
});
|
||||
|
||||
/*
|
||||
Test to verify that switching a plot's time context from global to
|
||||
its own independent time context and then back to global context works correctly.
|
||||
|
||||
After switching from fixed time mode (ITC) to real time mode (global context),
|
||||
the pause control for the plot should be available, indicating that it is following the right context.
|
||||
*/
|
||||
test('Plots follow the right time context', async ({ page }) => {
|
||||
// Set global time conductor to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// Ensure pause control is visible since global time conductor is in Real time mode.
|
||||
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
|
||||
|
||||
// Toggle independent time conductor ON
|
||||
await page.getByLabel('Enable Independent Time Conductor').click();
|
||||
|
||||
// Bring up the independent time conductor popup and switch to fixed time mode
|
||||
await page.getByLabel('Independent Time Conductor Settings').click();
|
||||
await page.getByLabel('Independent Time Conductor Mode Menu').click();
|
||||
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// Ensure pause control is no longer visible since the plot is following the independent time context
|
||||
await expect(page.getByTitle('Pause incoming real-time data')).toBeHidden();
|
||||
|
||||
// Toggle independent time conductor OFF - Note that the global time conductor is still in Real time mode
|
||||
await page.getByLabel('Disable Independent Time Conductor').click();
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// Ensure pause control is visible since the global time conductor is in real time mode
|
||||
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -31,6 +31,8 @@ import { expect, test } from '../../pluginFixtures.js';
|
||||
test.describe('Grand Search', () => {
|
||||
let grandSearchInput;
|
||||
|
||||
test.use({ ignore404s: [/_design\/object_names\/_view\/object_names$/] });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
grandSearchInput = page
|
||||
.getByLabel('OpenMCT Search')
|
||||
@ -191,7 +193,88 @@ test.describe('Grand Search', () => {
|
||||
await expect(searchResults).toContainText(folderName);
|
||||
});
|
||||
|
||||
test.describe('Search will test for the presence of the object_names index, and', () => {
|
||||
test('use index if available @couchdb @network', async ({ page }) => {
|
||||
await createObjectsForSearch(page);
|
||||
|
||||
let isObjectNamesViewAvailable = false;
|
||||
let isObjectNamesUsedForSearch = false;
|
||||
|
||||
page.on('request', async (request) => {
|
||||
const isObjectNamesRequest = request.url().endsWith('_view/object_names');
|
||||
const isHeadRequest = request.method().toLowerCase() === 'head';
|
||||
|
||||
if (isObjectNamesRequest && isHeadRequest) {
|
||||
const response = await request.response();
|
||||
isObjectNamesViewAvailable = response.status() === 200;
|
||||
}
|
||||
});
|
||||
|
||||
page.on('request', (request) => {
|
||||
const isObjectNamesRequest = request.url().endsWith('_view/object_names');
|
||||
const isPostRequest = request.method().toLowerCase() === 'post';
|
||||
|
||||
if (isObjectNamesRequest && isPostRequest) {
|
||||
isObjectNamesUsedForSearch = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Full search for object
|
||||
await grandSearchInput.pressSequentially('Clock', { delay: 100 });
|
||||
|
||||
// Wait for search to finish
|
||||
await waitForSearchCompletion(page);
|
||||
|
||||
expect(isObjectNamesViewAvailable).toBe(true);
|
||||
expect(isObjectNamesUsedForSearch).toBe(true);
|
||||
});
|
||||
|
||||
test('fall-back on base index if index not available @couchdb @network', async ({ page }) => {
|
||||
await page.route('**/_view/object_names', (route) => {
|
||||
route.fulfill({
|
||||
status: 404
|
||||
});
|
||||
});
|
||||
await createObjectsForSearch(page);
|
||||
|
||||
let isObjectNamesViewAvailable = false;
|
||||
let isFindUsedForSearch = false;
|
||||
|
||||
page.on('request', async (request) => {
|
||||
const isObjectNamesRequest = request.url().endsWith('_view/object_names');
|
||||
const isHeadRequest = request.method().toLowerCase() === 'head';
|
||||
|
||||
if (isObjectNamesRequest && isHeadRequest) {
|
||||
const response = await request.response();
|
||||
isObjectNamesViewAvailable = response.status() === 200;
|
||||
}
|
||||
});
|
||||
|
||||
page.on('request', (request) => {
|
||||
const isFindRequest = request.url().endsWith('_find');
|
||||
const isPostRequest = request.method().toLowerCase() === 'post';
|
||||
|
||||
if (isFindRequest && isPostRequest) {
|
||||
isFindUsedForSearch = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Full search for object
|
||||
await grandSearchInput.pressSequentially('Clock', { delay: 100 });
|
||||
|
||||
// Wait for search to finish
|
||||
await waitForSearchCompletion(page);
|
||||
console.info(
|
||||
`isObjectNamesViewAvailable: ${isObjectNamesViewAvailable} | isFindUsedForSearch: ${isFindUsedForSearch}`
|
||||
);
|
||||
expect(isObjectNamesViewAvailable).toBe(false);
|
||||
expect(isFindUsedForSearch).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('Search results are debounced @couchdb @network', async ({ page }) => {
|
||||
// Unfortunately 404s are always logged to the JavaScript console and can't be suppressed
|
||||
// A 404 is now thrown when we test for the presence of the object names view used by search.
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6179'
|
||||
@ -199,11 +282,17 @@ test.describe('Grand Search', () => {
|
||||
await createObjectsForSearch(page);
|
||||
|
||||
let networkRequests = [];
|
||||
|
||||
page.on('request', (request) => {
|
||||
const searchRequest =
|
||||
request.url().endsWith('_find') || request.url().includes('by_keystring');
|
||||
const fetchRequest = request.resourceType() === 'fetch';
|
||||
if (searchRequest && fetchRequest) {
|
||||
const isSearchRequest =
|
||||
request.url().endsWith('object_names') ||
|
||||
request.url().endsWith('_find') ||
|
||||
request.url().includes('by_keystring');
|
||||
const isFetchRequest = request.resourceType() === 'fetch';
|
||||
// CouchDB search results in a one-time head request to test for the presence of an index.
|
||||
const isHeadRequest = request.method().toLowerCase() === 'head';
|
||||
|
||||
if (isSearchRequest && isFetchRequest && !isHeadRequest) {
|
||||
networkRequests.push(request);
|
||||
}
|
||||
});
|
||||
|
@ -213,7 +213,6 @@ test.describe('Navigation memory leak is not detected in', () => {
|
||||
page,
|
||||
'example-imagery-memory-leak-test'
|
||||
);
|
||||
|
||||
// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@ -317,6 +316,12 @@ test.describe('Navigation memory leak is not detected in', () => {
|
||||
|
||||
// Manually invoke the garbage collector once all references are removed.
|
||||
window.gc();
|
||||
window.gc();
|
||||
window.gc();
|
||||
|
||||
setTimeout(() => {
|
||||
window.gc();
|
||||
}, 1000);
|
||||
|
||||
return gcPromise;
|
||||
});
|
||||
|
1260
package-lock.json
generated
1260
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@ -16,25 +16,25 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.23.3",
|
||||
"@braintree/sanitize-url": "6.0.4",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@types/d3-axis": "3.0.6",
|
||||
"@types/d3-scale": "4.0.8",
|
||||
"@types/d3-selection": "3.0.10",
|
||||
"@types/d3-shape": "3.0.0",
|
||||
"@types/d3-shape": "3.1.7",
|
||||
"@types/eventemitter3": "1.2.0",
|
||||
"@types/jasmine": "5.1.2",
|
||||
"@types/lodash": "4.17.0",
|
||||
"@vue/compiler-sfc": "3.4.3",
|
||||
"babel-loader": "9.1.0",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"babel-plugin-istanbul": "7.0.0",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"copy-webpack-plugin": "13.0.0",
|
||||
"cspell": "7.3.8",
|
||||
"css-loader": "6.10.0",
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "4.0.2",
|
||||
"d3-selection": "3.0.0",
|
||||
"d3-shape": "3.0.0",
|
||||
"d3-shape": "3.2.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-compat": "4.2.0",
|
||||
@ -51,7 +51,7 @@
|
||||
"git-rev-sync": "3.0.2",
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "5.0.0",
|
||||
"jasmine-core": "5.1.1",
|
||||
"jasmine-core": "5.6.0",
|
||||
"karma": "6.4.2",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
"karma-cli": "2.0.0",
|
||||
@ -64,14 +64,14 @@
|
||||
"karma-webpack": "5.0.1",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "12.0.0",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"marked": "15.0.7",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"moment": "2.30.1",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.41",
|
||||
"nano": "10.1.4",
|
||||
"npm-run-all2": "6.1.2",
|
||||
"nyc": "15.1.0",
|
||||
"npm-run-all2": "7.0.2",
|
||||
"nyc": "17.1.0",
|
||||
"painterro": "1.2.87",
|
||||
"plotly.js-basic-dist-min": "2.29.1",
|
||||
"plotly.js-gl2d-dist-min": "2.20.0",
|
||||
@ -79,21 +79,21 @@
|
||||
"prettier-eslint": "16.3.0",
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sanitize-html": "2.12.1",
|
||||
"sanitize-html": "2.15.0",
|
||||
"sass": "1.71.1",
|
||||
"sass-loader": "14.1.1",
|
||||
"style-loader": "3.3.3",
|
||||
"style-loader": "4.0.0",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"tiny-emitter": "2.1.0",
|
||||
"typescript": "5.3.3",
|
||||
"uuid": "9.0.1",
|
||||
"uuid": "11.1.0",
|
||||
"vue": "3.4.24",
|
||||
"vue-eslint-parser": "9.4.2",
|
||||
"vue-loader": "16.8.3",
|
||||
"webpack": "5.90.3",
|
||||
"webpack": "5.98.0",
|
||||
"webpack-cli": "5.1.1",
|
||||
"webpack-dev-server": "5.0.2",
|
||||
"webpack-merge": "5.10.0"
|
||||
"webpack-merge": "6.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./dist ./node_modules ./coverage ./html-test-results ./e2e/test-results ./.nyc_output ./e2e/.nyc_output",
|
||||
|
@ -20,6 +20,8 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { isIdentifier } from '../objects/object-utils';
|
||||
|
||||
/**
|
||||
* @typedef {import('openmct').DomainObject} DomainObject
|
||||
*/
|
||||
@ -209,9 +211,15 @@ export default class CompositionCollection {
|
||||
this.#cleanUpMutables();
|
||||
const children = await this.#provider.load(this.domainObject);
|
||||
const childObjects = await Promise.all(
|
||||
children.map((c) => this.#publicAPI.objects.get(c, abortSignal))
|
||||
children.map((child) => {
|
||||
if (isIdentifier(child)) {
|
||||
return this.#publicAPI.objects.get(child, abortSignal);
|
||||
} else {
|
||||
return Promise.resolve(child);
|
||||
}
|
||||
})
|
||||
);
|
||||
childObjects.forEach((c) => this.add(c, true));
|
||||
childObjects.forEach((child) => this.add(child, true));
|
||||
this.#emit('load');
|
||||
|
||||
return childObjects;
|
||||
|
@ -96,8 +96,9 @@ export default class CompositionProvider {
|
||||
* object.
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* for which to load composition
|
||||
* @returns {Promise<Identifier[]>} a promise for
|
||||
* the Identifiers in this composition
|
||||
* @returns {Promise<Identifier[] | DomainObject[]>} a promise for
|
||||
* the Identifiers or Domain Objects in this composition. If Identifiers are returned,
|
||||
* they will be automatically resolved to domain objects by the API.
|
||||
*/
|
||||
load(domainObject) {
|
||||
throw new Error('This method must be implemented by a subclass.');
|
||||
|
@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
import { toRaw } from 'vue';
|
||||
|
||||
import { makeKeyString } from '../objects/object-utils.js';
|
||||
import { makeKeyString, parseKeyString } from '../objects/object-utils.js';
|
||||
import CompositionProvider from './CompositionProvider.js';
|
||||
|
||||
/**
|
||||
@ -75,7 +75,11 @@ export default class DefaultCompositionProvider extends CompositionProvider {
|
||||
* the Identifiers in this composition
|
||||
*/
|
||||
load(domainObject) {
|
||||
return Promise.all(domainObject.composition);
|
||||
const identifiers = domainObject.composition
|
||||
.filter((idOrKeystring) => idOrKeystring !== null && idOrKeystring !== undefined)
|
||||
.map((idOrKeystring) => parseKeyString(idOrKeystring));
|
||||
|
||||
return Promise.all(identifiers);
|
||||
}
|
||||
/**
|
||||
* Attach listeners for changes to the composition of a given domain object.
|
||||
|
@ -27,6 +27,7 @@ import ConflictError from './ConflictError.js';
|
||||
import InMemorySearchProvider from './InMemorySearchProvider.js';
|
||||
import InterceptorRegistry from './InterceptorRegistry.js';
|
||||
import MutableDomainObject from './MutableDomainObject.js';
|
||||
import { isIdentifier, isKeyString } from './object-utils.js';
|
||||
import RootObjectProvider from './RootObjectProvider.js';
|
||||
import RootRegistry from './RootRegistry.js';
|
||||
import Transaction from './Transaction.js';
|
||||
@ -742,11 +743,19 @@ export default class ObjectAPI {
|
||||
* @param {AbortSignal} abortSignal (optional) signal to abort fetch requests
|
||||
* @returns {Promise<Array<DomainObject>>} a promise containing an array of domain objects
|
||||
*/
|
||||
async getOriginalPath(identifier, path = [], abortSignal = null) {
|
||||
const domainObject = await this.get(identifier, abortSignal);
|
||||
async getOriginalPath(identifierOrObject, path = [], abortSignal = null) {
|
||||
let domainObject;
|
||||
|
||||
if (isKeyString(identifierOrObject) || isIdentifier(identifierOrObject)) {
|
||||
domainObject = await this.get(identifierOrObject, abortSignal);
|
||||
} else {
|
||||
domainObject = identifierOrObject;
|
||||
}
|
||||
|
||||
if (!domainObject) {
|
||||
return [];
|
||||
}
|
||||
|
||||
path.push(domainObject);
|
||||
const { location } = domainObject;
|
||||
if (location && !this.#pathContainsDomainObject(location, path)) {
|
||||
|
@ -359,6 +359,18 @@ class IndependentTimeContext extends TimeContext {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
* @override
|
||||
*/
|
||||
isFixed() {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.isFixed(...arguments);
|
||||
} else {
|
||||
return super.isFixed(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
* @override
|
||||
@ -400,7 +412,7 @@ class IndependentTimeContext extends TimeContext {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the time context to the global time context
|
||||
* Reset the time context from the global time context
|
||||
*/
|
||||
resetContext() {
|
||||
if (this.upstreamTimeContext) {
|
||||
@ -428,6 +440,10 @@ class IndependentTimeContext extends TimeContext {
|
||||
// Emit bounds so that views that are changing context get the upstream bounds
|
||||
this.emit('bounds', this.getBounds());
|
||||
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
|
||||
// Also emit the mode in case it's different from previous time context
|
||||
if (this.getMode()) {
|
||||
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -502,6 +518,10 @@ class IndependentTimeContext extends TimeContext {
|
||||
// Emit bounds so that views that are changing context get the upstream bounds
|
||||
this.emit('bounds', this.getBounds());
|
||||
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.getBounds());
|
||||
// Also emit the mode in case it's different from the global time context
|
||||
if (this.getMode()) {
|
||||
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.getMode()));
|
||||
}
|
||||
// now that the view's context is set, tell others to check theirs in case they were following this view's context.
|
||||
this.globalTimeContext.emit('refreshContext', viewKey);
|
||||
}
|
||||
|
@ -23,6 +23,7 @@
|
||||
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '@/api/time/constants';
|
||||
import IndependentTimeContext from '@/api/time/IndependentTimeContext';
|
||||
|
||||
import { TIME_CONTEXT_EVENTS } from './constants';
|
||||
import GlobalTimeContext from './GlobalTimeContext.js';
|
||||
|
||||
/**
|
||||
@ -142,7 +143,7 @@ class TimeAPI extends GlobalTimeContext {
|
||||
addIndependentContext(key, value, clockKey) {
|
||||
let timeContext = this.getIndependentContext(key);
|
||||
|
||||
//stop following upstream time context since the view has it's own
|
||||
//stop following upstream time context since the view has its own
|
||||
timeContext.resetContext();
|
||||
|
||||
if (clockKey) {
|
||||
@ -152,6 +153,9 @@ class TimeAPI extends GlobalTimeContext {
|
||||
timeContext.setMode(FIXED_MODE_KEY, value);
|
||||
}
|
||||
|
||||
// Also emit the mode in case it's different from the previous time context
|
||||
timeContext.emit(TIME_CONTEXT_EVENTS.modeChanged, structuredClone(timeContext.getMode()));
|
||||
|
||||
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
|
||||
this.emit('refreshContext', key);
|
||||
|
||||
|
@ -24,6 +24,9 @@ export default function (folderName, couchPlugin, searchFilter) {
|
||||
location: 'ROOT'
|
||||
});
|
||||
}
|
||||
},
|
||||
search() {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
});
|
||||
|
||||
@ -35,9 +38,17 @@ export default function (folderName, couchPlugin, searchFilter) {
|
||||
);
|
||||
},
|
||||
load() {
|
||||
return couchProvider.getObjectsByFilter(searchFilter).then((objects) => {
|
||||
return objects.map((object) => object.identifier);
|
||||
});
|
||||
let searchResults;
|
||||
|
||||
if (searchFilter.viewName !== undefined) {
|
||||
// Use a view to search, instead of an _all_docs find
|
||||
searchResults = couchProvider.getObjectsByView(searchFilter);
|
||||
} else {
|
||||
// Use the _find endpoint to search _all_docs
|
||||
searchResults = couchProvider.getObjectsByFilter(searchFilter);
|
||||
}
|
||||
|
||||
return searchResults;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -344,12 +344,19 @@ export default {
|
||||
},
|
||||
beforeMount() {
|
||||
this.marked = new Marked();
|
||||
this.renderer = new this.marked.Renderer();
|
||||
this.marked.use({
|
||||
breaks: true,
|
||||
extensions: [
|
||||
{
|
||||
name: 'link',
|
||||
renderer: (options) => {
|
||||
return this.validateLink(options);
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
const originalLinkRenderer = this.renderer.link;
|
||||
this.renderer.link = this.validateLink.bind(this, originalLinkRenderer);
|
||||
|
||||
this.manageEmbedLayout = _.debounce(this.manageEmbedLayout, 400);
|
||||
|
||||
if (this.$refs.embedsWrapper) {
|
||||
@ -437,10 +444,7 @@ export default {
|
||||
}
|
||||
},
|
||||
convertMarkDownToHtml(text = '') {
|
||||
let markDownHtml = this.marked.parse(text, {
|
||||
breaks: true,
|
||||
renderer: this.renderer
|
||||
});
|
||||
let markDownHtml = this.marked.parse(text);
|
||||
markDownHtml = sanitizeHtml(markDownHtml, SANITIZATION_SCHEMA);
|
||||
return markDownHtml;
|
||||
},
|
||||
@ -451,21 +455,19 @@ export default {
|
||||
this.$refs.entryInput.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
},
|
||||
validateLink(originalLinkRenderer, href, title, text) {
|
||||
validateLink(options) {
|
||||
const { href, text } = options;
|
||||
try {
|
||||
const domain = new URL(href).hostname;
|
||||
const urlIsWhitelisted = this.urlWhitelist.some((partialDomain) => {
|
||||
return domain.endsWith(partialDomain);
|
||||
});
|
||||
|
||||
if (!urlIsWhitelisted) {
|
||||
return text;
|
||||
}
|
||||
const linkHtml = originalLinkRenderer.call(this.renderer, href, title, text);
|
||||
const linkHtmlWithTarget = linkHtml.replace(
|
||||
/^<a /,
|
||||
'<a class="c-hyperlink" target="_blank"'
|
||||
);
|
||||
return linkHtmlWithTarget;
|
||||
|
||||
return `<a class="c-hyperlink" target="_blank" href="${href}">${text}</a>`;
|
||||
} catch (error) {
|
||||
// had error parsing this URL, just return the text
|
||||
return text;
|
||||
|
@ -434,16 +434,69 @@ class CouchObjectProvider {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
async getObjectsByView({ designDoc, viewName, keysToSearch }, abortSignal) {
|
||||
const stringifiedKeys = JSON.stringify(keysToSearch);
|
||||
const url = `${this.url}/_design/${designDoc}/_view/${viewName}?keys=${stringifiedKeys}&include_docs=true`;
|
||||
async isViewDefined(designDoc, viewName) {
|
||||
const url = `${this.url}/_design/${designDoc}/_view/${viewName}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'HEAD'
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef GetObjectByViewOptions
|
||||
* @property {String} designDoc the name of the design document that the view belongs to
|
||||
* @property {String} viewName
|
||||
* @property {Array.<String>} [keysToSearch] a list of discrete view keys to search for. View keys are not object identifiers.
|
||||
* @property {String} [startKey] limit the search to a range of keys starting with the provided `startKey`. One of `keysToSearch` OR `startKey` AND `endKey` must be provided
|
||||
* @property {String} [endKey] limit the search to a range of keys ending with the provided `endKey`. One of `keysToSearch` OR `startKey` AND `endKey` must be provided
|
||||
* @property {Number} [limit] limit the number of results returned
|
||||
* @property {String} [objectIdField] The field (either key or value) to treat as an object key. If provided, include_docs will be set to false in the request, and the field will be used as an object identifier. A bulk request will be used to resolve objects from identifiers
|
||||
*/
|
||||
/**
|
||||
* Return objects based on a call to a view. See https://docs.couchdb.org/en/stable/api/ddoc/views.html.
|
||||
* @param {GetObjectByViewOptions} options
|
||||
* @param {AbortSignal} abortSignal
|
||||
* @returns {Promise<Array.<import('openmct.js').DomainObject>>}
|
||||
*/
|
||||
async getObjectsByView(
|
||||
{ designDoc, viewName, keysToSearch, startKey, endKey, limit, objectIdField },
|
||||
abortSignal
|
||||
) {
|
||||
let stringifiedKeys = JSON.stringify(keysToSearch);
|
||||
const url = `${this.url}/_design/${designDoc}/_view/${viewName}`;
|
||||
const requestBody = {};
|
||||
let requestBodyString;
|
||||
|
||||
if (objectIdField === undefined) {
|
||||
requestBody.include_docs = true;
|
||||
}
|
||||
|
||||
if (limit !== undefined) {
|
||||
requestBody.limit = limit;
|
||||
}
|
||||
|
||||
if (startKey !== undefined && endKey !== undefined) {
|
||||
/* spell-checker: disable */
|
||||
requestBody.startkey = startKey;
|
||||
requestBody.endkey = endKey;
|
||||
requestBodyString = JSON.stringify(requestBody);
|
||||
requestBodyString = requestBodyString.replace('$START_KEY', startKey);
|
||||
requestBodyString = requestBodyString.replace('$END_KEY', endKey);
|
||||
/* spell-checker: enable */
|
||||
} else {
|
||||
requestBody.keys = stringifiedKeys;
|
||||
requestBodyString = JSON.stringify(requestBody);
|
||||
}
|
||||
|
||||
let objectModels = [];
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: abortSignal
|
||||
signal: abortSignal,
|
||||
body: requestBodyString
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@ -454,13 +507,21 @@ class CouchObjectProvider {
|
||||
|
||||
const result = await response.json();
|
||||
const couchRows = result.rows;
|
||||
couchRows.forEach((couchRow) => {
|
||||
const couchDoc = couchRow.doc;
|
||||
const objectModel = this.#getModel(couchDoc);
|
||||
if (objectModel) {
|
||||
objectModels.push(objectModel);
|
||||
}
|
||||
});
|
||||
if (objectIdField !== undefined) {
|
||||
const objectIdsToResolve = [];
|
||||
couchRows.forEach((couchRow) => {
|
||||
objectIdsToResolve.push(couchRow[objectIdField]);
|
||||
});
|
||||
objectModels = Object.values(await this.#bulkGet(objectIdsToResolve), abortSignal);
|
||||
} else {
|
||||
couchRows.forEach((couchRow) => {
|
||||
const couchDoc = couchRow.doc;
|
||||
const objectModel = this.#getModel(couchDoc);
|
||||
if (objectModel) {
|
||||
objectModels.push(objectModel);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// do nothing
|
||||
}
|
||||
|
@ -33,7 +33,11 @@ class CouchSearchProvider {
|
||||
#bulkPromise;
|
||||
#batchIds;
|
||||
#lastAbortSignal;
|
||||
|
||||
#isSearchByNameViewDefined;
|
||||
/**
|
||||
*
|
||||
* @param {import('./CouchObjectProvider').default} couchObjectProvider
|
||||
*/
|
||||
constructor(couchObjectProvider) {
|
||||
this.couchObjectProvider = couchObjectProvider;
|
||||
this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES;
|
||||
@ -67,18 +71,47 @@ class CouchSearchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
searchForObjects(query, abortSignal) {
|
||||
const filter = {
|
||||
selector: {
|
||||
model: {
|
||||
name: {
|
||||
$regex: `(?i)${query}`
|
||||
#isOptimizedSearchByNameSupported() {
|
||||
let isOptimizedSearchAvailable;
|
||||
|
||||
if (this.#isSearchByNameViewDefined === undefined) {
|
||||
isOptimizedSearchAvailable = this.#isSearchByNameViewDefined =
|
||||
this.couchObjectProvider.isViewDefined('object_names', 'object_names');
|
||||
} else {
|
||||
isOptimizedSearchAvailable = this.#isSearchByNameViewDefined;
|
||||
}
|
||||
|
||||
return isOptimizedSearchAvailable;
|
||||
}
|
||||
|
||||
async searchForObjects(query, abortSignal) {
|
||||
const preparedQuery = query.toLowerCase().trim();
|
||||
const supportsOptimizedSearchByName = await this.#isOptimizedSearchByNameSupported();
|
||||
|
||||
if (supportsOptimizedSearchByName) {
|
||||
return this.couchObjectProvider.getObjectsByView(
|
||||
{
|
||||
designDoc: 'object_names',
|
||||
viewName: 'object_names',
|
||||
startKey: preparedQuery,
|
||||
endKey: preparedQuery + `\ufff0`,
|
||||
objectIdField: 'value',
|
||||
limit: 1000
|
||||
},
|
||||
abortSignal
|
||||
);
|
||||
} else {
|
||||
const filter = {
|
||||
selector: {
|
||||
model: {
|
||||
name: {
|
||||
$regex: `(?i)${query}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
||||
};
|
||||
return this.couchObjectProvider.getObjectsByFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
async #deferBatchAnnotationSearch() {
|
||||
|
@ -373,44 +373,6 @@ describe('the plugin', () => {
|
||||
expect(requestMethod).toEqual('PUT');
|
||||
});
|
||||
});
|
||||
describe('implements server-side search', () => {
|
||||
let mockPromise;
|
||||
beforeEach(() => {
|
||||
mockPromise = Promise.resolve({
|
||||
body: {
|
||||
getReader() {
|
||||
return {
|
||||
read() {
|
||||
return Promise.resolve({
|
||||
done: true,
|
||||
value: undefined
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
fetch.and.returnValue(mockPromise);
|
||||
});
|
||||
|
||||
it("using Couch's 'find' endpoint", async () => {
|
||||
await Promise.all(openmct.objects.search('test'));
|
||||
const requestUrl = fetch.calls.mostRecent().args[0];
|
||||
|
||||
// we only want one call to fetch, not 2!
|
||||
// see https://github.com/nasa/openmct/issues/4667
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(requestUrl.endsWith('_find')).toBeTrue();
|
||||
});
|
||||
|
||||
it('and supports search by object name', async () => {
|
||||
await Promise.all(openmct.objects.search('test'));
|
||||
const requestPayload = JSON.parse(fetch.calls.mostRecent().args[1].body);
|
||||
|
||||
expect(requestPayload).toBeDefined();
|
||||
expect(requestPayload.selector.model.name.$regex).toEqual('(?i)test');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('the view', () => {
|
||||
|
@ -99,6 +99,48 @@ create_replicator_table() {
|
||||
add_index_and_views() {
|
||||
echo "Adding index and views to $OPENMCT_DATABASE_NAME database"
|
||||
|
||||
# Add object names search index
|
||||
response=$(curl --silent --user "${CURL_USERPASS_ARG}" --request PUT "$COUCH_BASE_LOCAL"/"$OPENMCT_DATABASE_NAME"/_design/object_names/\
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"_id":"_design/object_names",
|
||||
"views":{
|
||||
"object_names":{
|
||||
"map":"function(doc) { if (doc.model && doc.model.name) { const name = doc.model.name.toLowerCase().trim(); if (name.length > 0) { emit(name, doc._id); const tokens = name.split(/[^a-zA-Z0-9]/); tokens.forEach((token) => { if (token.length > 0) { emit(token, doc._id); } }); } } }"
|
||||
}
|
||||
}
|
||||
}');
|
||||
|
||||
if [[ $response =~ "\"ok\":true" ]]; then
|
||||
echo "Successfully created object_names"
|
||||
elif [[ $response =~ "\"error\":\"conflict\"" ]]; then
|
||||
echo "object_names already exists, skipping creation"
|
||||
else
|
||||
echo "Unable to create object_names"
|
||||
echo $response
|
||||
fi
|
||||
|
||||
# Add object types search index
|
||||
response=$(curl --silent --user "${CURL_USERPASS_ARG}" --request PUT "$COUCH_BASE_LOCAL"/"$OPENMCT_DATABASE_NAME"/_design/object_types/\
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"_id":"_design/object_types",
|
||||
"views":{
|
||||
"object_types":{
|
||||
"map":"function(doc) { if (doc.model && doc.model.type) { const type = doc.model.type.toLowerCase().trim(); if (type.length > 0) { emit(type, null); } } }"
|
||||
}
|
||||
}
|
||||
}')
|
||||
|
||||
if [[ $response =~ "\"ok\":true" ]]; then
|
||||
echo "Successfully created object_types"
|
||||
elif [[ $response =~ "\"error\":\"conflict\"" ]]; then
|
||||
echo "object_types already exists, skipping creation"
|
||||
else
|
||||
echo "Unable to create object_types"
|
||||
echo $response
|
||||
fi
|
||||
|
||||
# Add type_tags_index
|
||||
response=$(curl --silent --user "${CURL_USERPASS_ARG}" --request POST "$COUCH_BASE_LOCAL"/"$OPENMCT_DATABASE_NAME"/_index/\
|
||||
--header 'Content-Type: application/json' \
|
||||
|
@ -539,6 +539,7 @@ export default {
|
||||
this.followTimeContext();
|
||||
},
|
||||
followTimeContext() {
|
||||
this.updateMode();
|
||||
this.updateDisplayBounds(this.timeContext.getBounds());
|
||||
this.timeContext.on('modeChanged', this.updateMode);
|
||||
this.timeContext.on('boundsChanged', this.updateDisplayBounds);
|
||||
|
@ -243,12 +243,20 @@ export default {
|
||||
this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setTimeOptionsMode);
|
||||
},
|
||||
setTimeOptionsClock(clock) {
|
||||
// If the user has persisted any time options, then don't override them with global settings.
|
||||
if (this.independentTCEnabled) {
|
||||
return;
|
||||
}
|
||||
this.setTimeOptionsOffsets();
|
||||
this.timeOptions.clock = clock.key;
|
||||
},
|
||||
setTimeOptionsMode(mode) {
|
||||
this.setTimeOptionsOffsets();
|
||||
this.timeOptions.mode = mode;
|
||||
// If the user has persisted any time options, then don't override them with global settings.
|
||||
if (this.independentTCEnabled) {
|
||||
this.setTimeOptionsOffsets();
|
||||
this.timeOptions.mode = mode;
|
||||
this.isFixed = this.timeOptions.mode === FIXED_MODE_KEY;
|
||||
}
|
||||
},
|
||||
setTimeOptionsOffsets() {
|
||||
this.timeOptions.clockOffsets =
|
||||
|
@ -21,7 +21,9 @@
|
||||
-->
|
||||
<template>
|
||||
<div ref="axisHolder" class="c-timesystem-axis">
|
||||
<div class="nowMarker" :style="nowMarkerStyle"><span class="icon-arrow-down"></span></div>
|
||||
<div class="nowMarker" :style="nowMarkerStyle" aria-label="Now Marker">
|
||||
<span class="icon-arrow-down"></span>
|
||||
</div>
|
||||
<svg :width="svgWidth" :height="svgHeight">
|
||||
<g class="axis" font-size="1.3em" :transform="axisTransform"></g>
|
||||
</svg>
|
||||
@ -116,8 +118,10 @@ export default {
|
||||
this.axisTransform = `translate(${this.alignmentData.leftWidth + leftOffset}, 20)`;
|
||||
|
||||
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
|
||||
|
||||
this.leftAlignmentOffset = this.alignmentData.leftWidth + leftOffset;
|
||||
this.alignmentOffset =
|
||||
this.alignmentData.leftWidth + leftOffset + this.alignmentData.rightWidth + rightOffset;
|
||||
this.leftAlignmentOffset + this.alignmentData.rightWidth + rightOffset;
|
||||
this.refresh();
|
||||
},
|
||||
deep: true
|
||||
@ -175,8 +179,8 @@ export default {
|
||||
this.nowMarkerStyle.height = this.contentHeight + 'px';
|
||||
const nowTimeStamp = this.openmct.time.now();
|
||||
const now = this.xScale(nowTimeStamp);
|
||||
this.nowMarkerStyle.left = `${now + this.alignmentOffset}px`;
|
||||
if (now > this.width) {
|
||||
this.nowMarkerStyle.left = `${now + this.leftAlignmentOffset}px`;
|
||||
if (now < 0 || now > this.width) {
|
||||
nowMarker.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
@ -111,9 +111,8 @@ export default {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyStringForObject = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
const originalPathObjects = await this.openmct.objects.getOriginalPath(
|
||||
keyStringForObject,
|
||||
domainObject,
|
||||
[],
|
||||
abortSignal
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user