mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 10:44:21 +00:00
Compare commits
77 Commits
eval-sourc
...
7936-add-d
Author | SHA1 | Date | |
---|---|---|---|
1aee416194 | |||
aa5fa468b5 | |||
3ad64f08c5 | |||
1ced12d22f | |||
9e68514b27 | |||
28b5d7c41c | |||
ecd120387c | |||
a6517bb33e | |||
1fde0d9e38 | |||
32a0e15691 | |||
0e940b2883 | |||
15b674f3d1 | |||
0933d27ce6 | |||
f163034e18 | |||
e6cb940ee7 | |||
cfa2129660 | |||
6cafa7a22d | |||
9522040929 | |||
5b28086f95 | |||
bb4fea78f5 | |||
5312458776 | |||
3c24205b67 | |||
65b1f0256d | |||
8c72e4a062 | |||
601fc33e75 | |||
638b03c17d | |||
531ef3ef1b | |||
68fc3172a0 | |||
51d96544ec | |||
546714b3dc | |||
099153ba4e | |||
27af030566 | |||
b865d8c038 | |||
2ae1fe1579 | |||
cba7c7f8ed | |||
49a106b79e | |||
f4ec532357 | |||
72ff0bced6 | |||
36d31973fe | |||
3af9083f89 | |||
2ba6bc9c73 | |||
aaa2e43796 | |||
6bda108e95 | |||
20426fe359 | |||
20247bbd87 | |||
62b4975d57 | |||
d048af108e | |||
cda7cc9d06 | |||
d97f7c347b | |||
781d83410a | |||
64bd625d0b | |||
3d3f093c7e | |||
38292953fc | |||
96d8870f22 | |||
aaec052783 | |||
6f26add740 | |||
052129ba87 | |||
d046ad13ff | |||
e9f120a480 | |||
0db301dea8 | |||
3b236cc33b | |||
8b5e2f4595 | |||
5b006b69b7 | |||
2776cc8ac9 | |||
55bed6a525 | |||
944634d759 | |||
7af3996d29 | |||
7b22cf3371 | |||
5be103ea72 | |||
680b0953b2 | |||
3159de08b1 | |||
d74e1b19b6 | |||
5bb6a18cd4 | |||
14b947c101 | |||
61b982ab99 | |||
ba4d8a428b | |||
ea9947cab5 |
@ -483,7 +483,8 @@
|
||||
"countup",
|
||||
"darkmatter",
|
||||
"Undeletes",
|
||||
"SSSZ"
|
||||
"SSSZ",
|
||||
"pageerror"
|
||||
],
|
||||
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
|
||||
"ignorePaths": [
|
||||
|
10
.github/workflows/e2e-couchdb.yml
vendored
10
.github/workflows/e2e-couchdb.yml
vendored
@ -51,7 +51,7 @@ 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
|
||||
|
||||
@ -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()
|
||||
|
@ -682,6 +682,21 @@ async function linkParameterToObject(page, parameterName, objectName) {
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the currently viewed `domainObject` from the browse bar.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} newName
|
||||
*/
|
||||
async function renameCurrentObjectFromBrowseBar(page, newName) {
|
||||
const nameInput = page.getByLabel('Browse bar object name');
|
||||
await nameInput.click();
|
||||
await nameInput.fill('');
|
||||
await nameInput.fill(newName);
|
||||
// Click the browse bar container to save changes
|
||||
await page.getByLabel('Browse bar', { exact: true }).click();
|
||||
}
|
||||
|
||||
export {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject,
|
||||
@ -693,6 +708,7 @@ export {
|
||||
linkParameterToObject,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
navigateToObjectWithRealTime,
|
||||
renameCurrentObjectFromBrowseBar,
|
||||
setEndOffset,
|
||||
setFixedIndependentTimeConductorBounds,
|
||||
setFixedTimeMode,
|
||||
|
@ -0,0 +1,67 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createDomainObjectWithDefaults, setRealTimeMode } from '../../../appActions.js';
|
||||
import { MISSION_TIME } from '../../../constants.js';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
const TELEMETRY_RATE = 2500;
|
||||
|
||||
test.describe('Example Event Generator Acknowledge with Controlled Clock @clock', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await setRealTimeMode(page);
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Event Message Generator with Acknowledge'
|
||||
});
|
||||
});
|
||||
|
||||
test('Rows are updatable in place', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7938'
|
||||
});
|
||||
|
||||
await test.step('First telemetry datum gets added as new row', async () => {
|
||||
await page.clock.fastForward(TELEMETRY_RATE);
|
||||
const rows = page.getByLabel('table content').getByLabel('Table Row');
|
||||
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
|
||||
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(acknowledgeCell).not.toHaveAttribute('title', 'OK');
|
||||
});
|
||||
|
||||
await test.step('Incoming Telemetry datum matching an existing rows in place update key has data merged to existing row', async () => {
|
||||
await page.clock.fastForward(TELEMETRY_RATE * 2);
|
||||
const rows = page.getByLabel('table content').getByLabel('Table Row');
|
||||
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
|
||||
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(acknowledgeCell).toHaveAttribute('title', 'OK');
|
||||
});
|
||||
});
|
||||
});
|
@ -126,7 +126,7 @@ test.describe('Gantt Chart', () => {
|
||||
await page.goto(ganttChart.url);
|
||||
|
||||
// Assert that the Plan's status is displayed as draft
|
||||
expect(await page.locator('.u-contents.c-swimlane.is-status--draft').count()).toBe(
|
||||
expect(await page.locator('.c-swimlane.is-status--draft').count()).toBe(
|
||||
Object.keys(testPlan1).length
|
||||
);
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -287,6 +287,41 @@ test.describe('Basic Condition Set Use', () => {
|
||||
description: 'https://github.com/nasa/openmct/issues/7484'
|
||||
});
|
||||
});
|
||||
|
||||
test('ConditionSet has add criteria button enabled/disabled when composition is and is not available', async ({
|
||||
page
|
||||
}) => {
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.goto(conditionSet.url);
|
||||
// Change the object to edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Create a condition
|
||||
await page.locator('#addCondition').click();
|
||||
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
|
||||
|
||||
// Validate that the add criteria button is disabled
|
||||
await expect(page.getByLabel('Add Criteria - Disabled')).toHaveAttribute('disabled');
|
||||
|
||||
// Add Telemetry to ConditionSet
|
||||
const sineWaveGeneratorTreeItem = page
|
||||
.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
})
|
||||
.getByRole('treeitem', {
|
||||
name: exampleTelemetry.name
|
||||
});
|
||||
const conditionCollection = page.locator('#conditionCollection');
|
||||
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
|
||||
|
||||
// Validate that the add criteria button is enabled and adds a new criterion
|
||||
await expect(page.getByLabel('Add Criteria - Enabled')).not.toHaveAttribute('disabled');
|
||||
await page.getByLabel('Add Criteria - Enabled').click();
|
||||
const numOfUnnamedCriteria = await page.getByLabel('Criterion Telemetry Selection').count();
|
||||
expect(numOfUnnamedCriteria).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Condition Set Composition', () => {
|
||||
|
@ -507,8 +507,140 @@ test.describe('Display Layout', () => {
|
||||
// In real time mode, we don't fetch annotations at all
|
||||
await expect.poll(() => networkRequests, { timeout: 10000 }).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Same objects with different request options have unique subscriptions', async ({
|
||||
page
|
||||
}) => {
|
||||
// Expand My Items
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
|
||||
// Create a Display Layout
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display'
|
||||
});
|
||||
|
||||
// Create a State Generator, set to higher frequency updates
|
||||
const stateGenerator = await createDomainObjectWithDefaults(page, {
|
||||
type: 'State Generator',
|
||||
name: 'State Generator'
|
||||
});
|
||||
const stateGeneratorTreeItem = page.getByRole('treeitem', {
|
||||
name: stateGenerator.name
|
||||
});
|
||||
await stateGeneratorTreeItem.click({ button: 'right' });
|
||||
await page.getByLabel('Edit Properties...').click();
|
||||
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('0.1');
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Create a Table for filtering ON values
|
||||
const tableFilterOnValue = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
name: 'Table Filter On Value'
|
||||
});
|
||||
const tableFilterOnTreeItem = page.getByRole('treeitem', {
|
||||
name: tableFilterOnValue.name
|
||||
});
|
||||
|
||||
// Create a Table for filtering OFF values
|
||||
const tableFilterOffValue = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
name: 'Table Filter Off Value'
|
||||
});
|
||||
const tableFilterOffTreeItem = page.getByRole('treeitem', {
|
||||
name: tableFilterOffValue.name
|
||||
});
|
||||
|
||||
// Navigate to ON filtering table and add state generator and setup filters
|
||||
await page.goto(tableFilterOnValue.url);
|
||||
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
||||
await selectFilterOption(page, '1');
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Navigate to OFF filtering table and add state generator and setup filters
|
||||
await page.goto(tableFilterOffValue.url);
|
||||
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
||||
await selectFilterOption(page, '0');
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Navigate to the display layout and edit it
|
||||
await page.goto(displayLayout.url);
|
||||
|
||||
// Add the tables to the display layout
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await tableFilterOffTreeItem.dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 10, y: 300 }
|
||||
});
|
||||
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 400, y: 500 },
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true
|
||||
});
|
||||
await tableFilterOnTreeItem.dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 10, y: 100 }
|
||||
});
|
||||
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 400, y: 300 },
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true
|
||||
});
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Get the tables so we can verify filtering is working as expected
|
||||
const tableFilterOn = page.getByLabel(`${tableFilterOnValue.name} Frame`, {
|
||||
exact: true
|
||||
});
|
||||
const tableFilterOff = page.getByLabel(`${tableFilterOffValue.name} Frame`, {
|
||||
exact: true
|
||||
});
|
||||
|
||||
// Verify filtering is working correctly
|
||||
|
||||
// Check that no filtered values appear for at least 2 seconds
|
||||
const VERIFICATION_TIME = 2000; // 2 seconds
|
||||
const CHECK_INTERVAL = 100; // Check every 100ms
|
||||
|
||||
// Create a promise that will check for filtered values periodically
|
||||
const checkForCorrectValues = new Promise((resolve, reject) => {
|
||||
const interval = setInterval(async () => {
|
||||
const offCount = await tableFilterOn.locator('td[title="OFF"]').count();
|
||||
const onCount = await tableFilterOff.locator('td[title="ON"]').count();
|
||||
if (offCount > 0 || onCount > 0) {
|
||||
clearInterval(interval);
|
||||
reject(
|
||||
new Error(
|
||||
`Found ${offCount} OFF and ${onCount} ON values when expecting 0 OFF and 0 ON`
|
||||
)
|
||||
);
|
||||
}
|
||||
}, CHECK_INTERVAL);
|
||||
|
||||
// After VERIFICATION_TIME, if no filtered values were found, resolve successfully
|
||||
setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}, VERIFICATION_TIME);
|
||||
});
|
||||
|
||||
await expect(checkForCorrectValues).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
async function selectFilterOption(page, filterOption) {
|
||||
await page.getByRole('tab', { name: 'Filters' }).click();
|
||||
await page
|
||||
.getByLabel('Inspector Views')
|
||||
.locator('li')
|
||||
.filter({ hasText: 'State Generator' })
|
||||
.locator('span')
|
||||
.click();
|
||||
await page.getByRole('switch').click();
|
||||
await page.selectOption('select[name="setSelectionThreshold"]', filterOption);
|
||||
}
|
||||
|
||||
async function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LAYOUT_NAME) {
|
||||
await expect(page.getByLabel(layoutObject, { exact: true })).toHaveCount(0);
|
||||
await addLayoutObject(page, DISPLAY_LAYOUT_NAME, layoutObject);
|
||||
|
110
e2e/tests/functional/plugins/event/eventTimelineView.e2e.spec.js
Normal file
110
e2e/tests/functional/plugins/event/eventTimelineView.e2e.spec.js
Normal file
@ -0,0 +1,110 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createDomainObjectWithDefaults, setTimeConductorBounds } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Event Timeline View', () => {
|
||||
let eventTimelineView;
|
||||
let eventGenerator1;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
eventTimelineView = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Time Strip'
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: eventTimelineView.uuid
|
||||
});
|
||||
|
||||
eventGenerator1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Event Message Generator',
|
||||
parent: eventTimelineView.uuid
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Event Message Generator with Acknowledge',
|
||||
parent: eventTimelineView.uuid
|
||||
});
|
||||
|
||||
await setTimeConductorBounds(page, {
|
||||
startDate: '2024-01-01',
|
||||
endDate: '2024-01-01',
|
||||
startTime: '01:01:00',
|
||||
endTime: '01:04:00'
|
||||
});
|
||||
});
|
||||
|
||||
test('Ensure we can build a Time Strip with event', async ({ page }) => {
|
||||
await page.goto(eventTimelineView.url);
|
||||
|
||||
// click on an event
|
||||
await page
|
||||
.getByLabel(eventTimelineView.name)
|
||||
.getByLabel(/PROGRAM ALARM/)
|
||||
.click();
|
||||
|
||||
// click on the event inspector tab
|
||||
await page.getByRole('tab', { name: 'Event' }).click();
|
||||
|
||||
// ensure the event inspector has the the same event
|
||||
await expect(page.getByText(/PROGRAM ALARM/)).toBeVisible();
|
||||
|
||||
// count the event lines
|
||||
const eventWrappersContainer = page.locator('.c-events-tsv__container');
|
||||
const eventWrappers = eventWrappersContainer.locator('.c-events-tsv__event-line');
|
||||
const expectedEventWrappersCount = 25;
|
||||
await expect(eventWrappers).toHaveCount(expectedEventWrappersCount);
|
||||
|
||||
// click on another event
|
||||
await page
|
||||
.getByLabel(eventTimelineView.name)
|
||||
.getByLabel(/pegged/)
|
||||
.click();
|
||||
|
||||
// ensure the tooltip shows up
|
||||
await expect(
|
||||
page.getByRole('tooltip').getByText(/pegged on horizontal velocity/)
|
||||
).toBeVisible();
|
||||
|
||||
// and that event appears in the inspector
|
||||
await expect(
|
||||
page.getByLabel('Inspector Views').getByText(/pegged on horizontal velocity/)
|
||||
).toBeVisible();
|
||||
|
||||
// turn on extended lines
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: `Toggle extended event lines overlay for ${eventGenerator1.name}`
|
||||
})
|
||||
.click();
|
||||
|
||||
// count the extended lines
|
||||
const overlayLinesContainer = page.locator('.c-timeline__overlay-lines');
|
||||
const extendedLines = overlayLinesContainer.locator('.c-timeline__event-line--extended');
|
||||
const expectedCount = 25;
|
||||
await expect(extendedLines).toHaveCount(expectedCount);
|
||||
});
|
||||
});
|
@ -28,7 +28,9 @@ import { v4 as uuid } from 'uuid';
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject
|
||||
createExampleTelemetryObject,
|
||||
setRealTimeMode,
|
||||
setStartOffset
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
@ -166,6 +168,57 @@ test.describe('Gauge', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Gauge does not break when an object is missing', async ({ page }) => {
|
||||
// Set up error listeners
|
||||
const pageErrors = [];
|
||||
|
||||
// Listen for uncaught exceptions
|
||||
page.on('pageerror', (err) => {
|
||||
pageErrors.push(err.message);
|
||||
});
|
||||
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Create a Gauge
|
||||
const gauge = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Gauge',
|
||||
name: 'Gauge with missing object'
|
||||
});
|
||||
|
||||
// Create a Sine Wave Generator in the Gauge with a loading delay
|
||||
const missingSWG = await createExampleTelemetryObject(page, gauge.uuid);
|
||||
|
||||
// Remove the object from local storage
|
||||
await page.evaluate(
|
||||
([missingObject]) => {
|
||||
const mct = localStorage.getItem('mct');
|
||||
const mctObjects = JSON.parse(mct);
|
||||
delete mctObjects[missingObject.uuid];
|
||||
localStorage.setItem('mct', JSON.stringify(mctObjects));
|
||||
},
|
||||
[missingSWG]
|
||||
);
|
||||
|
||||
// Verify start bounds
|
||||
await expect(page.getByLabel('Start offset: 00:30:00')).toBeVisible();
|
||||
|
||||
// Nav to the Gauge
|
||||
await page.goto(gauge.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// adjust time bounds and ensure they are updated
|
||||
await setStartOffset(page, {
|
||||
startHours: '00',
|
||||
startMins: '45',
|
||||
startSecs: '00'
|
||||
});
|
||||
|
||||
// Verify start bounds changed
|
||||
await expect(page.getByLabel('Start offset: 00:45:00')).toBeVisible();
|
||||
|
||||
// // Verify no errors were thrown
|
||||
expect(pageErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Gauge enforces composition policy', async ({ page }) => {
|
||||
// Create a Gauge
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
|
@ -26,7 +26,10 @@ This test suite is dedicated to tests which verify the basic operations surround
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
renameCurrentObjectFromBrowseBar
|
||||
} from '../../../../appActions.js';
|
||||
import { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';
|
||||
import * as nbUtils from '../../../../helper/notebookUtils.js';
|
||||
import { expect, streamToString, test } from '../../../../pluginFixtures.js';
|
||||
@ -596,4 +599,61 @@ test.describe('Notebook entry tests', () => {
|
||||
await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
|
||||
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('When changing the name of a notebook in the browse bar, new notebook changes are not lost', async ({
|
||||
page
|
||||
}) => {
|
||||
const TEST_TEXT = 'Do not lose me!';
|
||||
const FIRST_NEW_NAME = 'New Name';
|
||||
const SECOND_NEW_NAME = 'Second New Name';
|
||||
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
|
||||
await renameCurrentObjectFromBrowseBar(page, FIRST_NEW_NAME);
|
||||
|
||||
// verify the name change in tree and browse bar
|
||||
await verifyNameChange(page, FIRST_NEW_NAME);
|
||||
|
||||
// enter one entry
|
||||
await enterAndCommitTextEntry(page, TEST_TEXT);
|
||||
|
||||
// verify the entry is present
|
||||
await expect(await page.locator(`text="${TEST_TEXT}"`).count()).toEqual(1);
|
||||
|
||||
// change the name
|
||||
await renameCurrentObjectFromBrowseBar(page, SECOND_NEW_NAME);
|
||||
|
||||
// verify the name change in tree and browse bar
|
||||
await verifyNameChange(page, SECOND_NEW_NAME);
|
||||
|
||||
// verify the entry is still present
|
||||
await expect(await page.locator(`text="${TEST_TEXT}"`).count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Enter text into the last notebook entry and commit it.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} text
|
||||
*/
|
||||
async function enterAndCommitTextEntry(page, text) {
|
||||
await nbUtils.addNotebookEntry(page);
|
||||
await nbUtils.enterTextInLastEntry(page, text);
|
||||
await nbUtils.commitEntry(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the name change in the tree and browse bar.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} newName
|
||||
*/
|
||||
async function verifyNameChange(page, newName) {
|
||||
await expect(
|
||||
page.getByRole('treeitem').locator('.is-navigated-object .c-tree__item__name')
|
||||
).toHaveText(newName);
|
||||
await expect(page.getByLabel('Browse bar object name')).toHaveText(newName);
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,58 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2025, 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
* This test suite is dedicated to testing the rendering and interaction of plots.
|
||||
*
|
||||
*/
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Plot Controls in compact mode', () => {
|
||||
let timeStrip;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
timeStrip = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Time Strip'
|
||||
});
|
||||
|
||||
// Create an overlay plot with a sine wave generator
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: timeStrip.uuid
|
||||
});
|
||||
await page.goto(`${timeStrip.url}`);
|
||||
});
|
||||
|
||||
test('Plots show cursor guides', async ({ page }) => {
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// click on cursor guides control
|
||||
await page.getByTitle('Toggle cursor guides').click();
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
await expect(page.getByLabel('Vertical cursor guide')).toBeVisible();
|
||||
await expect(page.getByLabel('Horizontal cursor guide')).toBeVisible();
|
||||
});
|
||||
});
|
88
example/eventGenerator/EventLimitProvider.js
Normal file
88
example/eventGenerator/EventLimitProvider.js
Normal file
@ -0,0 +1,88 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
export const SEVERITY_CSS = {
|
||||
WATCH: 'is-event--yellow',
|
||||
WARNING: 'is-event--yellow',
|
||||
DISTRESS: 'is-event--red',
|
||||
CRITICAL: 'is-event--red',
|
||||
SEVERE: 'is-event--purple'
|
||||
};
|
||||
|
||||
const NOMINAL_SEVERITY = {
|
||||
cssClass: 'is-event--no-style',
|
||||
name: 'NOMINAL'
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} EvaluationResult
|
||||
* @property {string} cssClass CSS class information
|
||||
* @property {string} name a severity name
|
||||
*/
|
||||
export default class EventLimitProvider {
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
}
|
||||
|
||||
getLimitEvaluator(domainObject) {
|
||||
const self = this;
|
||||
|
||||
return {
|
||||
/**
|
||||
* Evaluates a telemetry datum for severity.
|
||||
*
|
||||
* @param {Datum} datum the telemetry datum from the historical or realtime plugin ({@link Datum})
|
||||
* @param {object} valueMetadata metadata about the telemetry datum
|
||||
*
|
||||
* @returns {EvaluationResult} ({@link EvaluationResult})
|
||||
*/
|
||||
evaluate: function (datum, valueMetadata) {
|
||||
// prevent applying the class to the tr, only to td
|
||||
if (!valueMetadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (datum.severity in SEVERITY_CSS) {
|
||||
return self.getSeverity(datum, valueMetadata);
|
||||
}
|
||||
|
||||
return NOMINAL_SEVERITY;
|
||||
}
|
||||
};
|
||||
}
|
||||
getSeverity(datum, valueMetadata) {
|
||||
if (!valueMetadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
const severityValue = datum.severity;
|
||||
|
||||
return {
|
||||
cssClass: SEVERITY_CSS[severityValue],
|
||||
name: severityValue
|
||||
};
|
||||
}
|
||||
|
||||
supportsLimits(domainObject) {
|
||||
return domainObject.type === 'eventGenerator';
|
||||
}
|
||||
}
|
@ -41,11 +41,32 @@ class EventMetadataProvider {
|
||||
{
|
||||
key: 'message',
|
||||
name: 'Message',
|
||||
format: 'string'
|
||||
format: 'string',
|
||||
hints: {
|
||||
label: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const inPlaceUpdateMetadataValue = {
|
||||
key: 'messageId',
|
||||
name: 'row identifier',
|
||||
format: 'string',
|
||||
useToUpdateInPlace: true
|
||||
};
|
||||
const eventAcknowledgeMetadataValue = {
|
||||
key: 'acknowledge',
|
||||
name: 'Acknowledge',
|
||||
format: 'string'
|
||||
};
|
||||
|
||||
const eventGeneratorWithAcknowledge = structuredClone(this.METADATA_BY_TYPE.eventGenerator);
|
||||
eventGeneratorWithAcknowledge.values.push(inPlaceUpdateMetadataValue);
|
||||
eventGeneratorWithAcknowledge.values.push(eventAcknowledgeMetadataValue);
|
||||
|
||||
this.METADATA_BY_TYPE.eventGeneratorWithAcknowledge = eventGeneratorWithAcknowledge;
|
||||
}
|
||||
|
||||
supportsMetadata(domainObject) {
|
||||
|
@ -24,8 +24,11 @@
|
||||
* Module defining EventTelemetryProvider. Created by chacskaylo on 06/18/2015.
|
||||
*/
|
||||
|
||||
import { SEVERITY_CSS } from './EventLimitProvider.js';
|
||||
import messages from './transcript.json';
|
||||
|
||||
const DUR_MIN = 1000;
|
||||
const DUR_MAX = 10000;
|
||||
class EventTelemetryProvider {
|
||||
constructor() {
|
||||
this.defaultSize = 25;
|
||||
@ -33,14 +36,23 @@ class EventTelemetryProvider {
|
||||
|
||||
generateData(firstObservedTime, count, startTime, duration, name) {
|
||||
const millisecondsSinceStart = startTime - firstObservedTime;
|
||||
const utc = startTime + count * duration;
|
||||
const randStartDelay = Math.max(DUR_MIN, Math.random() * DUR_MAX);
|
||||
const utc = startTime + randStartDelay + count * duration;
|
||||
const ind = count % messages.length;
|
||||
const message = messages[ind] + ' - [' + millisecondsSinceStart + ']';
|
||||
// pick a random severity level + 1 for an undefined level so we can do nominal
|
||||
const severity =
|
||||
Math.random() > 0.4
|
||||
? Object.keys(SEVERITY_CSS)[
|
||||
Math.floor(Math.random() * Object.keys(SEVERITY_CSS).length + 1)
|
||||
]
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
name,
|
||||
utc,
|
||||
message
|
||||
message,
|
||||
severity
|
||||
};
|
||||
}
|
||||
|
||||
@ -53,7 +65,7 @@ class EventTelemetryProvider {
|
||||
}
|
||||
|
||||
subscribe(domainObject, callback) {
|
||||
const duration = domainObject.telemetry.duration * 1000;
|
||||
const duration = domainObject.telemetry.duration * DUR_MIN;
|
||||
const firstObservedTime = Date.now();
|
||||
let count = 0;
|
||||
|
||||
@ -78,7 +90,7 @@ class EventTelemetryProvider {
|
||||
request(domainObject, options) {
|
||||
let start = options.start;
|
||||
const end = Math.min(Date.now(), options.end); // no future values
|
||||
const duration = domainObject.telemetry.duration * 1000;
|
||||
const duration = domainObject.telemetry.duration * DUR_MIN;
|
||||
const size = options.size ? options.size : this.defaultSize;
|
||||
const data = [];
|
||||
const firstObservedTime = options.start;
|
||||
|
@ -0,0 +1,70 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Module defining EventTelemetryProvider. Created by chacskaylo on 06/18/2015.
|
||||
*/
|
||||
|
||||
import EventTelemetryProvider from './EventTelemetryProvider.js';
|
||||
|
||||
class EventWithAcknowledgeTelemetryProvider extends EventTelemetryProvider {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.unAcknowledgedData = undefined;
|
||||
}
|
||||
|
||||
generateData(firstObservedTime, count, startTime, duration, name) {
|
||||
if (this.unAcknowledgedData === undefined) {
|
||||
const unAcknowledgedData = super.generateData(
|
||||
firstObservedTime,
|
||||
count,
|
||||
startTime,
|
||||
duration,
|
||||
name
|
||||
);
|
||||
unAcknowledgedData.messageId = unAcknowledgedData.message;
|
||||
this.unAcknowledgedData = unAcknowledgedData;
|
||||
|
||||
return this.unAcknowledgedData;
|
||||
} else {
|
||||
const acknowledgedData = {
|
||||
...this.unAcknowledgedData,
|
||||
acknowledge: 'OK'
|
||||
};
|
||||
|
||||
this.unAcknowledgedData = undefined;
|
||||
|
||||
return acknowledgedData;
|
||||
}
|
||||
}
|
||||
|
||||
supportsRequest(domainObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
supportsSubscribe(domainObject) {
|
||||
return domainObject.type === 'eventGeneratorWithAcknowledge';
|
||||
}
|
||||
}
|
||||
|
||||
export default EventWithAcknowledgeTelemetryProvider;
|
@ -19,8 +19,10 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import EventLimitProvider from './EventLimitProvider.js';
|
||||
import EventMetadataProvider from './EventMetadataProvider.js';
|
||||
import EventTelemetryProvider from './EventTelemetryProvider.js';
|
||||
import EventWithAcknowledgeTelemetryProvider from './EventWithAcknowledgeTelemetryProvider.js';
|
||||
|
||||
export default function EventGeneratorPlugin(options) {
|
||||
return function install(openmct) {
|
||||
@ -38,5 +40,22 @@ export default function EventGeneratorPlugin(options) {
|
||||
});
|
||||
openmct.telemetry.addProvider(new EventTelemetryProvider());
|
||||
openmct.telemetry.addProvider(new EventMetadataProvider());
|
||||
|
||||
openmct.types.addType('eventGeneratorWithAcknowledge', {
|
||||
name: 'Event Message Generator with Acknowledge',
|
||||
description:
|
||||
'For development use. Creates sample event message data stream and updates the event row with an acknowledgement.',
|
||||
cssClass: 'icon-generator-events',
|
||||
creatable: true,
|
||||
initialize: function (object) {
|
||||
object.telemetry = {
|
||||
duration: 2.5
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
openmct.telemetry.addProvider(new EventWithAcknowledgeTelemetryProvider());
|
||||
|
||||
openmct.telemetry.addProvider(new EventLimitProvider(openmct));
|
||||
};
|
||||
}
|
||||
|
@ -108,6 +108,16 @@ const METADATA_BY_TYPE = {
|
||||
string: 'ON'
|
||||
}
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
singleSelectionThreshold: true,
|
||||
comparator: 'equals',
|
||||
possibleValues: [
|
||||
{ label: 'OFF', value: 0 },
|
||||
{ label: 'ON', value: 1 }
|
||||
]
|
||||
}
|
||||
],
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
|
@ -34,14 +34,16 @@ StateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
|
||||
return domainObject.type === 'example.state-generator';
|
||||
};
|
||||
|
||||
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
|
||||
StateGeneratorProvider.prototype.subscribe = function (domainObject, callback, options) {
|
||||
var duration = domainObject.telemetry.duration * 1000;
|
||||
|
||||
var interval = setInterval(function () {
|
||||
var interval = setInterval(() => {
|
||||
var now = Date.now();
|
||||
var datum = pointForTimestamp(now, duration, domainObject.name);
|
||||
datum.value = String(datum.value);
|
||||
callback(datum);
|
||||
if (!this.shouldBeFiltered(datum, options)) {
|
||||
datum.value = String(datum.value);
|
||||
callback(datum);
|
||||
}
|
||||
}, duration);
|
||||
|
||||
return function () {
|
||||
@ -63,9 +65,25 @@ StateGeneratorProvider.prototype.request = function (domainObject, options) {
|
||||
|
||||
var data = [];
|
||||
while (start <= end && data.length < 5000) {
|
||||
data.push(pointForTimestamp(start, duration, domainObject.name));
|
||||
const point = pointForTimestamp(start, duration, domainObject.name);
|
||||
|
||||
if (!this.shouldBeFiltered(point, options)) {
|
||||
data.push(point);
|
||||
}
|
||||
start += duration;
|
||||
}
|
||||
|
||||
return Promise.resolve(data);
|
||||
};
|
||||
|
||||
StateGeneratorProvider.prototype.shouldBeFiltered = function (point, options) {
|
||||
const valueToFilter = options?.filters?.state?.equals?.[0];
|
||||
|
||||
if (!valueToFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { value } = point;
|
||||
|
||||
return value !== Number(valueToFilter);
|
||||
};
|
||||
|
@ -113,7 +113,8 @@
|
||||
creatable: true
|
||||
})
|
||||
);
|
||||
openmct.install(openmct.plugins.Timeline());
|
||||
const timeLinePlugin = openmct.plugins.Timeline();
|
||||
openmct.install(timeLinePlugin);
|
||||
openmct.install(openmct.plugins.Hyperlink());
|
||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
||||
openmct.install(
|
||||
@ -234,6 +235,7 @@
|
||||
openmct.install(openmct.plugins.Timelist());
|
||||
openmct.install(openmct.plugins.BarChart());
|
||||
openmct.install(openmct.plugins.ScatterPlot());
|
||||
openmct.install(openmct.plugins.EventTimestripPlugin(timeLinePlugin.extendedLinesBus));
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
openmct.start();
|
||||
});
|
||||
|
@ -250,6 +250,90 @@ export default class TelemetryAPI {
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes objects for consistent serialization by:
|
||||
* 1. Removing non-plain objects (class instances) and functions
|
||||
* 2. Sorting object keys alphabetically to ensure consistent ordering
|
||||
*/
|
||||
sanitizeForSerialization(key, value) {
|
||||
// Handle null and primitives directly
|
||||
if (value === null || typeof value !== 'object') {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Remove functions and non-plain objects (except arrays)
|
||||
if (
|
||||
typeof value === 'function' ||
|
||||
(Object.getPrototypeOf(value) !== Object.prototype && !Array.isArray(value))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// For plain objects, just sort the keys
|
||||
if (!Array.isArray(value)) {
|
||||
const sortedObject = {};
|
||||
const sortedKeys = Object.keys(value).sort();
|
||||
|
||||
sortedKeys.forEach((objectKey) => {
|
||||
sortedObject[objectKey] = value[objectKey];
|
||||
});
|
||||
|
||||
return sortedObject;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a numeric hash value for an options object. The hash is consistent
|
||||
* for equivalent option objects regardless of property order.
|
||||
*
|
||||
* This is used to create compact, unique cache keys for telemetry subscriptions with
|
||||
* different options configurations. The hash function ensures that identical options
|
||||
* objects will always generate the same hash value, while different options objects
|
||||
* (even with small differences) will generate different hash values.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} options The options object to hash
|
||||
* @returns {number} A positive integer hash of the options object
|
||||
*/
|
||||
#hashOptions(options) {
|
||||
const sanitizedOptionsString = JSON.stringify(
|
||||
options,
|
||||
this.sanitizeForSerialization.bind(this)
|
||||
);
|
||||
|
||||
let hash = 0;
|
||||
const prime = 31;
|
||||
const modulus = 1e9 + 9; // Large prime number
|
||||
|
||||
for (let i = 0; i < sanitizedOptionsString.length; i++) {
|
||||
const char = sanitizedOptionsString.charCodeAt(i);
|
||||
// Calculate new hash value while keeping numbers manageable
|
||||
hash = Math.floor((hash * prime + char) % modulus);
|
||||
}
|
||||
|
||||
return Math.abs(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique cache key for a telemetry subscription based on the
|
||||
* domain object identifier and options (which includes strategy).
|
||||
*
|
||||
* Uses a hash of the options object to create compact cache keys while still
|
||||
* ensuring unique keys for different subscription configurations.
|
||||
*
|
||||
* @private
|
||||
* @param {import('openmct').DomainObject} domainObject The domain object being subscribed to
|
||||
* @param {Object} options The subscription options object (including strategy)
|
||||
* @returns {string} A unique key string for caching the subscription
|
||||
*/
|
||||
#getSubscriptionCacheKey(domainObject, options) {
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
|
||||
return `${keyString}:${this.#hashOptions(options)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request
|
||||
* The request will be modified when it is received and will be returned in it's modified state
|
||||
@ -418,16 +502,14 @@ export default class TelemetryAPI {
|
||||
this.#subscribeCache = {};
|
||||
}
|
||||
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
const supportedStrategy = supportsBatching ? requestedStrategy : SUBSCRIBE_STRATEGY.LATEST;
|
||||
// Override the requested strategy with the strategy supported by the provider
|
||||
const optionsWithSupportedStrategy = {
|
||||
...options,
|
||||
strategy: supportedStrategy
|
||||
};
|
||||
// If batching is supported, we need to cache a subscription for each strategy -
|
||||
// latest and batched.
|
||||
const cacheKey = `${keyString}:${supportedStrategy}`;
|
||||
|
||||
const cacheKey = this.#getSubscriptionCacheKey(domainObject, optionsWithSupportedStrategy);
|
||||
let subscriber = this.#subscribeCache[cacheKey];
|
||||
|
||||
if (!subscriber) {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -27,10 +27,11 @@ import TooltipComponent from './components/TooltipComponent.vue';
|
||||
|
||||
class Tooltip extends EventEmitter {
|
||||
constructor(
|
||||
{ toolTipText, toolTipLocation, parentElement } = {
|
||||
{ toolTipText, toolTipLocation, parentElement, cssClasses } = {
|
||||
tooltipText: '',
|
||||
toolTipLocation: 'below',
|
||||
parentElement: null
|
||||
parentElement: null,
|
||||
cssClasses: []
|
||||
}
|
||||
) {
|
||||
super();
|
||||
@ -42,7 +43,8 @@ class Tooltip extends EventEmitter {
|
||||
provide: {
|
||||
toolTipText,
|
||||
toolTipLocation,
|
||||
parentElement
|
||||
parentElement,
|
||||
cssClasses
|
||||
},
|
||||
template: '<tooltip-component toolTipText="toolTipText"></tooltip-component>'
|
||||
});
|
||||
|
@ -80,10 +80,11 @@ class TooltipAPI {
|
||||
* @property {string} tooltipText text to show in the tooltip
|
||||
* @property {TOOLTIP_LOCATIONS} tooltipLocation location to show the tooltip relative to the parentElement
|
||||
* @property {HTMLElement} parentElement reference to the DOM node we're adding the tooltip to
|
||||
* @property {Array} cssClasses css classes to use with the tool tip element
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tooltips take an options object that consists of the string, tooltipLocation, and parentElement
|
||||
* Tooltips take an options object that consists of the string, tooltipLocation, a parentElement, and an array of cssClasses
|
||||
* @param {TooltipOptions} options
|
||||
*/
|
||||
tooltip(options) {
|
||||
|
@ -22,7 +22,8 @@ at runtime from the About dialog for additional information.
|
||||
<template>
|
||||
<div
|
||||
ref="tooltip-wrapper"
|
||||
class="c-menu c-tooltip-wrapper"
|
||||
class="c-tooltip-wrapper"
|
||||
:class="cssClasses"
|
||||
:style="toolTipLocationStyle"
|
||||
role="tooltip"
|
||||
aria-labelledby="tooltip-text"
|
||||
@ -36,7 +37,7 @@ at runtime from the About dialog for additional information.
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['toolTipText', 'toolTipLocation', 'parentElement'],
|
||||
inject: ['toolTipText', 'toolTipLocation', 'parentElement', 'cssClasses'],
|
||||
computed: {
|
||||
toolTipCoordinates() {
|
||||
return this.parentElement.getBoundingClientRect();
|
||||
|
@ -1,9 +1,13 @@
|
||||
.c-tooltip-wrapper {
|
||||
@include menuOuter();
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
width: auto;
|
||||
padding: $interiorMargin;
|
||||
padding: $interiorMargin $interiorMarginLg;
|
||||
overflow-wrap: break-word;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.c-tooltip {
|
||||
|
@ -48,7 +48,7 @@ const tooltipHelpers = {
|
||||
.reverse()
|
||||
.join(' / ');
|
||||
},
|
||||
buildToolTip(tooltipText, tooltipLocation, elementRef) {
|
||||
buildToolTip(tooltipText, tooltipLocation, elementRef, cssClasses) {
|
||||
if (!tooltipText || tooltipText.length < 1) {
|
||||
return;
|
||||
}
|
||||
@ -59,7 +59,8 @@ const tooltipHelpers = {
|
||||
this.tooltip = this.openmct.tooltips.tooltip({
|
||||
toolTipText: tooltipText,
|
||||
toolTipLocation: tooltipLocation,
|
||||
parentElement: parentElement
|
||||
parentElement: parentElement,
|
||||
cssClasses
|
||||
});
|
||||
},
|
||||
hideToolTip() {
|
||||
|
@ -160,8 +160,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="c-cdef__separator c-row-separator"></div>
|
||||
<div class="c-cdef__controls" :disabled="!telemetry.length">
|
||||
<div class="c-cdef__controls">
|
||||
<button
|
||||
:disabled="!telemetry.length"
|
||||
:aria-label="`Add Criteria - ${!telemetry.length ? 'Disabled' : 'Enabled'}`"
|
||||
class="c-cdef__add-criteria-button c-button c-button--labeled icon-plus"
|
||||
@click="addCriteria"
|
||||
>
|
||||
|
55
src/plugins/events/EventInspectorViewProvider.js
Normal file
55
src/plugins/events/EventInspectorViewProvider.js
Normal file
@ -0,0 +1,55 @@
|
||||
import mount from 'utils/mount';
|
||||
|
||||
import EventInspectorView from './components/EventInspectorView.vue';
|
||||
|
||||
export default function EventInspectorViewProvider(openmct) {
|
||||
const TIMELINE_VIEW = 'time-strip.event.inspector';
|
||||
return {
|
||||
key: TIMELINE_VIEW,
|
||||
name: 'Event',
|
||||
canView: function (selection) {
|
||||
if (selection.length === 0 || selection[0].length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectionType = selection[0][0].context?.type;
|
||||
const event = selection[0][0].context?.event;
|
||||
return selectionType === 'time-strip-event-selection' && event;
|
||||
},
|
||||
view: function (selection) {
|
||||
let _destroy = null;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
const { destroy } = mount(
|
||||
{
|
||||
el: element,
|
||||
components: {
|
||||
EventInspectorView
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject: selection[0][0].context.item,
|
||||
event: selection[0][0].context.event
|
||||
},
|
||||
template: '<event-inspector-view></event-inspector-view>'
|
||||
},
|
||||
{
|
||||
app: openmct.app,
|
||||
element
|
||||
}
|
||||
);
|
||||
_destroy = destroy;
|
||||
},
|
||||
priority: function () {
|
||||
return openmct.priority.HIGH;
|
||||
},
|
||||
destroy: function () {
|
||||
if (_destroy) {
|
||||
_destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
101
src/plugins/events/EventTimelineViewProvider.js
Normal file
101
src/plugins/events/EventTimelineViewProvider.js
Normal file
@ -0,0 +1,101 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import mount from 'utils/mount';
|
||||
|
||||
import EventTimelineView from './components/EventTimelineView.vue';
|
||||
|
||||
export default function EventTimestripViewProvider(openmct, extendedLinesBus) {
|
||||
const type = 'event.time-line.view';
|
||||
|
||||
function hasEventTelemetry(domainObject) {
|
||||
const metadata = openmct.telemetry.getMetadata(domainObject);
|
||||
if (!metadata) {
|
||||
return false;
|
||||
}
|
||||
const hasDomain = metadata.valuesForHints(['domain']).length > 0;
|
||||
const hasNoRange = !metadata.valuesForHints(['range'])?.length;
|
||||
|
||||
// for the moment, let's also exclude telemetry with images
|
||||
const hasNoImages = !metadata.valuesForHints(['image']).length;
|
||||
|
||||
return hasDomain && hasNoRange && hasNoImages;
|
||||
}
|
||||
|
||||
return {
|
||||
key: type,
|
||||
name: 'Event Timeline View',
|
||||
cssClass: 'icon-event',
|
||||
priority: function () {
|
||||
// We want this to be higher priority than the TelemetryTableView
|
||||
return openmct.priority.HIGH;
|
||||
},
|
||||
canView: function (domainObject, objectPath) {
|
||||
const isChildOfTimeStrip = objectPath.some((object) => object.type === 'time-strip');
|
||||
|
||||
return (
|
||||
hasEventTelemetry(domainObject) &&
|
||||
isChildOfTimeStrip &&
|
||||
!openmct.router.isNavigatedObject(objectPath)
|
||||
);
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
let _destroy = null;
|
||||
let component = null;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
const { vNode, destroy } = mount(
|
||||
{
|
||||
el: element,
|
||||
components: {
|
||||
EventTimelineView
|
||||
},
|
||||
provide: {
|
||||
openmct: openmct,
|
||||
domainObject: domainObject,
|
||||
objectPath: objectPath,
|
||||
extendedLinesBus
|
||||
},
|
||||
template: '<event-timeline-view ref="root"></event-timeline-view>'
|
||||
},
|
||||
{
|
||||
app: openmct.app,
|
||||
element
|
||||
}
|
||||
);
|
||||
_destroy = destroy;
|
||||
component = vNode.componentInstance;
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
if (_destroy) {
|
||||
_destroy();
|
||||
}
|
||||
},
|
||||
|
||||
getComponent() {
|
||||
return component;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
45
src/plugins/events/components/EventInspectorView.vue
Normal file
45
src/plugins/events/components/EventInspectorView.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="c-timelist-properties">
|
||||
<div class="c-inspect-properties">
|
||||
<ul class="c-inspect-properties__section">
|
||||
<div class="c-inspect-properties_header" title="'Details'">Details</div>
|
||||
<li
|
||||
v-for="[key, value] in Object.entries(event)"
|
||||
:key="key"
|
||||
class="c-inspect-properties__row"
|
||||
>
|
||||
<span class="c-inspect-properties__label">{{ key }}</span>
|
||||
<span class="c-inspect-properties__value">{{ value }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'event']
|
||||
};
|
||||
</script>
|
370
src/plugins/events/components/EventTimelineView.vue
Normal file
370
src/plugins/events/components/EventTimelineView.vue
Normal file
@ -0,0 +1,370 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div ref="events" class="c-events-tsv js-events-tsv" :style="alignmentStyle">
|
||||
<swim-lane v-if="eventItems.length" :is-nested="true" :hide-label="true">
|
||||
<template v-slot:object>
|
||||
<div ref="eventsContainer" class="c-events-tsv__container">
|
||||
<div
|
||||
v-for="event in eventItems"
|
||||
:id="`wrapper-${event.time}`"
|
||||
:ref="`wrapper-${event.time}`"
|
||||
:key="event.id"
|
||||
:aria-label="titleKey ? `${event[titleKey]}` : ''"
|
||||
class="c-events-tsv__event-line"
|
||||
:class="event.limitClass || ''"
|
||||
:style="`left: ${event.left}px`"
|
||||
@mouseover="showToolTip(event)"
|
||||
@mouseleave="dismissToolTip()"
|
||||
@click.stop="createSelectionForInspector(event)"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
</swim-lane>
|
||||
<div v-else class="c-timeline__no-items">No events within timeframe</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { scaleLinear, scaleUtc } from 'd3-scale';
|
||||
import _ from 'lodash';
|
||||
import { inject } from 'vue';
|
||||
|
||||
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
|
||||
|
||||
import { useAlignment } from '../../../ui/composables/alignmentContext.js';
|
||||
import eventData from '../mixins/eventData.js';
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||
|
||||
const PADDING = 1;
|
||||
const AXES_PADDING = 20;
|
||||
|
||||
export default {
|
||||
components: { SwimLane },
|
||||
mixins: [eventData, tooltipHelpers],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'extendedLinesBus'],
|
||||
setup() {
|
||||
const domainObject = inject('domainObject');
|
||||
const objectPath = inject('objectPath');
|
||||
const openmct = inject('openmct');
|
||||
const { alignment: alignmentData } = useAlignment(domainObject, objectPath, openmct);
|
||||
return { alignmentData };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
eventItems: [],
|
||||
eventHistory: [],
|
||||
titleKey: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
alignmentStyle() {
|
||||
let leftOffset = 0;
|
||||
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
|
||||
if (this.alignmentData.leftWidth) {
|
||||
leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
|
||||
}
|
||||
return {
|
||||
margin: `0 ${this.alignmentData.rightWidth + rightOffset}px 0 ${this.alignmentData.leftWidth + leftOffset}px`
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
eventHistory: {
|
||||
handler() {
|
||||
this.updateEventItems();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
alignmentData: {
|
||||
handler() {
|
||||
this.setScaleAndPlotEvents(this.timeSystem);
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.valueMetadata = {};
|
||||
this.height = 0;
|
||||
this.timeSystem = this.openmct.time.getTimeSystem();
|
||||
this.extendLines = false;
|
||||
},
|
||||
mounted() {
|
||||
this.setDimensions();
|
||||
this.setTimeContext();
|
||||
|
||||
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
if (metadata) {
|
||||
this.valueMetadata =
|
||||
metadata.valuesForHints(['range'])[0] || this.firstNonDomainAttribute(metadata);
|
||||
}
|
||||
// title is in the metadata, and is either a "hint" with a "label", or failing that, the first string type we find
|
||||
this.titleKey =
|
||||
metadata.valuesForHints(['label'])?.[0]?.key ||
|
||||
metadata.values().find((metadatum) => metadatum.format === 'string')?.key;
|
||||
|
||||
this.updateViewBounds();
|
||||
|
||||
this.resize = _.debounce(this.resize, 400);
|
||||
this.eventStripResizeObserver = new ResizeObserver(this.resize);
|
||||
this.eventStripResizeObserver.observe(this.$refs.events);
|
||||
|
||||
this.extendedLinesBus.on('disable-extended-lines', this.disableExtendEventLines);
|
||||
this.extendedLinesBus.on('enable-extended-lines', this.enableExtendEventLines);
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.eventStripResizeObserver) {
|
||||
this.eventStripResizeObserver.disconnect();
|
||||
}
|
||||
|
||||
this.stopFollowingTimeContext();
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
if (this.destroyEventContainer) {
|
||||
this.destroyEventContainer();
|
||||
}
|
||||
|
||||
this.extendedLinesBus.off('disable-extended-lines', this.disableExtendEventLines);
|
||||
this.extendedLinesBus.off('enable-extended-lines', this.enableExtendEventLines);
|
||||
this.extendedLinesBus.off('event-hovered', this.checkIfOurEvent);
|
||||
},
|
||||
methods: {
|
||||
setTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
this.timeContext.on('timeSystem', this.setScaleAndPlotEvents);
|
||||
this.timeContext.on('boundsChanged', this.updateViewBounds);
|
||||
},
|
||||
enableExtendEventLines(keyStringToEnable) {
|
||||
if (this.keyString === keyStringToEnable) {
|
||||
this.extendLines = true;
|
||||
this.emitExtendedLines();
|
||||
}
|
||||
},
|
||||
disableExtendEventLines(keyStringToDisable) {
|
||||
if (this.keyString === keyStringToDisable) {
|
||||
this.extendLines = false;
|
||||
// emit an empty array to clear the lines
|
||||
this.emitExtendedLines();
|
||||
}
|
||||
},
|
||||
firstNonDomainAttribute(metadata) {
|
||||
return metadata
|
||||
.values()
|
||||
.find((metadatum) => !metadatum.hints.domain && metadatum.key !== 'name');
|
||||
},
|
||||
stopFollowingTimeContext() {
|
||||
if (this.timeContext) {
|
||||
this.timeContext.off('timeSystem', this.setScaleAndPlotEvents);
|
||||
this.timeContext.off('boundsChanged', this.updateViewBounds);
|
||||
}
|
||||
},
|
||||
resize() {
|
||||
const clientWidth = this.getClientWidth();
|
||||
if (clientWidth !== this.width) {
|
||||
this.setDimensions();
|
||||
this.setScaleAndPlotEvents(this.timeSystem);
|
||||
}
|
||||
},
|
||||
getClientWidth() {
|
||||
let clientWidth = this.$refs.events.clientWidth;
|
||||
|
||||
if (!clientWidth) {
|
||||
//this is a hack - need a better way to find the parent of this component
|
||||
const parent = this.openmct.layout.$refs.browseObject.$el;
|
||||
if (parent) {
|
||||
clientWidth = parent.getBoundingClientRect().width;
|
||||
}
|
||||
}
|
||||
|
||||
return clientWidth;
|
||||
},
|
||||
updateViewBounds(bounds, isTick) {
|
||||
this.viewBounds = this.timeContext.getBounds();
|
||||
|
||||
if (!this.timeSystem) {
|
||||
this.timeSystem = this.timeContext.getTimeSystem();
|
||||
}
|
||||
|
||||
this.setScaleAndPlotEvents(this.timeSystem, !isTick);
|
||||
},
|
||||
setScaleAndPlotEvents(timeSystem) {
|
||||
if (timeSystem) {
|
||||
this.timeSystem = timeSystem;
|
||||
this.timeFormatter = this.getFormatter(this.timeSystem.key);
|
||||
}
|
||||
|
||||
this.setScale(this.timeSystem);
|
||||
this.updateEventItems();
|
||||
},
|
||||
getFormatter(key) {
|
||||
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
|
||||
const metadataValue = metadata.value(key) || { format: key };
|
||||
const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
return valueFormatter;
|
||||
},
|
||||
updateEventItems() {
|
||||
if (this.xScale) {
|
||||
this.eventItems = this.eventHistory.map((eventHistoryItem) => {
|
||||
const limitClass = this.getLimitClass(eventHistoryItem);
|
||||
return {
|
||||
...eventHistoryItem,
|
||||
left: this.xScale(eventHistoryItem.time),
|
||||
limitClass
|
||||
};
|
||||
});
|
||||
if (this.extendLines) {
|
||||
this.emitExtendedLines();
|
||||
}
|
||||
}
|
||||
},
|
||||
setDimensions() {
|
||||
const eventsHolder = this.$refs.events;
|
||||
this.width = this.getClientWidth();
|
||||
|
||||
this.height = Math.round(eventsHolder.getBoundingClientRect().height);
|
||||
},
|
||||
setScale(timeSystem) {
|
||||
if (!this.width) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!timeSystem) {
|
||||
timeSystem = this.timeContext.getTimeSystem();
|
||||
}
|
||||
|
||||
if (timeSystem.isUTCBased) {
|
||||
this.xScale = scaleUtc();
|
||||
this.xScale.domain([new Date(this.viewBounds.start), new Date(this.viewBounds.end)]);
|
||||
} else {
|
||||
this.xScale = scaleLinear();
|
||||
this.xScale.domain([this.viewBounds.start, this.viewBounds.end]);
|
||||
}
|
||||
|
||||
this.xScale.range([PADDING, this.width - PADDING * 2]);
|
||||
},
|
||||
createPathSelection(eventWrapper) {
|
||||
const selection = [];
|
||||
selection.unshift({
|
||||
element: eventWrapper,
|
||||
context: {
|
||||
item: this.domainObject
|
||||
}
|
||||
});
|
||||
this.objectPath.forEach((pathObject) => {
|
||||
selection.push({
|
||||
element: this.openmct.layout.$refs.browseObject.$el,
|
||||
context: {
|
||||
item: pathObject
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return selection;
|
||||
},
|
||||
setSelection() {
|
||||
let childContext = {};
|
||||
childContext.item = this.childObject;
|
||||
this.context = childContext;
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
|
||||
this.removeSelectable = this.openmct.selection.selectable(this.$el, this.context);
|
||||
},
|
||||
createSelectionForInspector(event) {
|
||||
|
||||
const eventWrapper = this.$refs[`wrapper-${event.time}`][0];
|
||||
const eventContext = {
|
||||
type: 'time-strip-event-selection',
|
||||
event
|
||||
};
|
||||
|
||||
const selection = this.createPathSelection(eventWrapper);
|
||||
if (
|
||||
selection.length &&
|
||||
this.openmct.objects.areIdsEqual(
|
||||
selection[0].context.item.identifier,
|
||||
this.domainObject.identifier
|
||||
)
|
||||
) {
|
||||
selection[0].context = {
|
||||
...selection[0].context,
|
||||
...eventContext
|
||||
};
|
||||
} else {
|
||||
selection.unshift({
|
||||
element: eventWrapper,
|
||||
context: {
|
||||
item: this.domainObject,
|
||||
...eventContext
|
||||
}
|
||||
});
|
||||
}
|
||||
this.openmct.selection.select(selection, true);
|
||||
},
|
||||
getLimitClass(event) {
|
||||
const limitEvaluation = this.limitEvaluator.evaluate(event, this.valueMetadata);
|
||||
return limitEvaluation?.cssClass;
|
||||
},
|
||||
showToolTip(event) {
|
||||
const aClasses = ['c-events-tooltip'];
|
||||
if (event.limitClass) {
|
||||
aClasses.push(event.limitClass);
|
||||
}
|
||||
const showToLeft = false; // Temp, stubbed in
|
||||
if (showToLeft) {
|
||||
aClasses.push('--left');
|
||||
}
|
||||
|
||||
this.buildToolTip(
|
||||
this.titleKey ? `${event[this.titleKey]}` : '',
|
||||
this.openmct.tooltips.TOOLTIP_LOCATIONS.RIGHT,
|
||||
`wrapper-${event.time}`,
|
||||
[aClasses.join(' ')]
|
||||
);
|
||||
this.extendedLinesBus.updateHoverExtendEventLine(this.keyString, event.time);
|
||||
},
|
||||
dismissToolTip() {
|
||||
this.hideToolTip();
|
||||
this.extendedLinesBus.updateHoverExtendEventLine(this.keyString, null);
|
||||
},
|
||||
emitExtendedLines() {
|
||||
let lines = [];
|
||||
if (this.extendLines) {
|
||||
lines = this.eventItems.map((e) => ({
|
||||
x: e.left,
|
||||
limitClass: e.limitClass,
|
||||
id: e.time
|
||||
}));
|
||||
}
|
||||
this.extendedLinesBus.updateExtendedLines(this.keyString, lines);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
111
src/plugins/events/components/events-view.scss
Normal file
111
src/plugins/events/components/events-view.scss
Normal file
@ -0,0 +1,111 @@
|
||||
@mixin styleEventLine($colorConst) {
|
||||
background-color: $colorConst !important;
|
||||
transition: box-shadow 250ms ease-out;
|
||||
&:hover,
|
||||
&[s-selected] {
|
||||
box-shadow: rgba($colorConst, 0.5) 0 0 0px 4px;
|
||||
transition: none;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
@mixin styleEventLineExtended($colorConst) {
|
||||
background-color: $colorConst !important;
|
||||
}
|
||||
|
||||
.c-events-tsv {
|
||||
$m: $interiorMargin;
|
||||
overflow: hidden;
|
||||
@include abs();
|
||||
|
||||
&__container {
|
||||
// Holds event lines
|
||||
background-color: $colorPlotBg;
|
||||
//box-shadow: inset $colorPlotAreaBorder 0 0 0 1px; // Using box-shadow instead of border to not affect box size
|
||||
position: absolute;
|
||||
top: $m; right: 0; bottom: $m; left: 0;
|
||||
}
|
||||
|
||||
&__event-line {
|
||||
// Wraps an individual event line
|
||||
// Also holds the hover flyout element
|
||||
$c: $colorEventLine;
|
||||
$lineW: $eventLineW;
|
||||
$hitAreaW: 7px;
|
||||
$m: $interiorMarginSm;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
top: $m; bottom: $m;
|
||||
width: $lineW;
|
||||
z-index: 1;
|
||||
|
||||
@include styleEventLine($colorEventLine);
|
||||
&.is-event {
|
||||
&--purple {
|
||||
@include styleEventLine($colorEventPurpleLine);
|
||||
}
|
||||
&--red {
|
||||
@include styleEventLine($colorEventRedLine);
|
||||
}
|
||||
&--orange {
|
||||
@include styleEventLine($colorEventOrangeLine);
|
||||
}
|
||||
&--yellow {
|
||||
@include styleEventLine($colorEventYellowLine);
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
// Extend hit area
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0; bottom: 0;
|
||||
z-index: 0;
|
||||
width: $hitAreaW;
|
||||
transform: translateX(($hitAreaW - $lineW) * -0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-events-canvas {
|
||||
pointer-events: auto;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
// Extended event lines
|
||||
.c-timeline__event-line--extended {
|
||||
@include abs();
|
||||
width: $eventLineW;
|
||||
opacity: 0.4;
|
||||
|
||||
&.--hilite {
|
||||
opacity: 0.8;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@include styleEventLineExtended($colorEventLine);
|
||||
&.is-event {
|
||||
&--purple {
|
||||
@include styleEventLineExtended($colorEventPurpleLine);
|
||||
}
|
||||
&--red {
|
||||
@include styleEventLineExtended($colorEventRedLine);
|
||||
}
|
||||
&--orange {
|
||||
@include styleEventLineExtended($colorEventOrangeLine);
|
||||
}
|
||||
&--yellow {
|
||||
@include styleEventLineExtended($colorEventYellowLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-events-tooltip {
|
||||
// Default to right of event line
|
||||
border-radius: 0 !important;
|
||||
//transform: translate(0, $interiorMargin);
|
||||
}
|
183
src/plugins/events/mixins/eventData.js
Normal file
183
src/plugins/events/mixins/eventData.js
Normal file
@ -0,0 +1,183 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
import { TIME_CONTEXT_EVENTS } from '../../../api/time/constants.js';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||
mounted() {
|
||||
// listen
|
||||
this.boundsChanged = this.boundsChanged.bind(this);
|
||||
this.timeSystemChanged = this.timeSystemChanged.bind(this);
|
||||
this.setDataTimeContext = this.setDataTimeContext.bind(this);
|
||||
this.openmct.objectViews.on('clearData', this.dataCleared);
|
||||
|
||||
// Get metadata and formatters
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
|
||||
this.durationFormatter = this.getFormatter(
|
||||
this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
|
||||
);
|
||||
|
||||
// initialize
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
this.setDataTimeContext();
|
||||
this.loadTelemetry();
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
delete this.unsubscribe;
|
||||
}
|
||||
|
||||
this.stopFollowingDataTimeContext();
|
||||
this.openmct.objectViews.off('clearData', this.dataCleared);
|
||||
|
||||
this.telemetryCollection.off('add', this.dataAdded);
|
||||
this.telemetryCollection.off('remove', this.dataRemoved);
|
||||
this.telemetryCollection.off('clear', this.dataCleared);
|
||||
|
||||
this.telemetryCollection.destroy();
|
||||
},
|
||||
methods: {
|
||||
dataAdded(addedItems, addedItemIndices) {
|
||||
const normalizedDataToAdd = addedItems.map((datum) => this.normalizeDatum(datum));
|
||||
let newEventHistory = this.eventHistory.slice();
|
||||
normalizedDataToAdd.forEach((datum, index) => {
|
||||
newEventHistory.splice(addedItemIndices[index] ?? -1, 0, datum);
|
||||
});
|
||||
//Assign just once so eventHistory watchers don't get called too often
|
||||
this.eventHistory = newEventHistory;
|
||||
},
|
||||
dataCleared() {
|
||||
this.eventHistory = [];
|
||||
},
|
||||
dataRemoved(removed) {
|
||||
const removedTimestamps = {};
|
||||
removed.forEach((_removed) => {
|
||||
const removedTimestamp = this.parseTime(_removed);
|
||||
removedTimestamps[removedTimestamp] = true;
|
||||
});
|
||||
|
||||
this.eventHistory = this.eventHistory.filter((event) => {
|
||||
const eventTimestamp = this.parseTime(event);
|
||||
|
||||
return !removedTimestamps[eventTimestamp];
|
||||
});
|
||||
},
|
||||
setDataTimeContext() {
|
||||
this.stopFollowingDataTimeContext();
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsChanged);
|
||||
this.timeContext.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.timeSystemChanged);
|
||||
},
|
||||
stopFollowingDataTimeContext() {
|
||||
if (this.timeContext) {
|
||||
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsChanged);
|
||||
this.timeContext.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.timeSystemChanged);
|
||||
}
|
||||
},
|
||||
formatEventUrl(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.eventFormatter.format(datum);
|
||||
},
|
||||
formatEventThumbnailUrl(datum) {
|
||||
if (!datum || !this.eventThumbnailFormatter) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.eventThumbnailFormatter.format(datum);
|
||||
},
|
||||
formatTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dateTimeStr = this.timeFormatter.format(datum);
|
||||
|
||||
// Replace ISO "T" with a space to allow wrapping
|
||||
return dateTimeStr.replace('T', ' ');
|
||||
},
|
||||
getEventDownloadName(datum) {
|
||||
let eventDownloadName = '';
|
||||
if (datum) {
|
||||
const key = this.eventDownloadNameMetadataValue.key;
|
||||
eventDownloadName = datum[key];
|
||||
}
|
||||
|
||||
return eventDownloadName;
|
||||
},
|
||||
parseTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.timeFormatter.parse(datum);
|
||||
},
|
||||
loadTelemetry() {
|
||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
|
||||
timeContext: this.timeContext
|
||||
});
|
||||
this.telemetryCollection.on('add', this.dataAdded);
|
||||
this.telemetryCollection.on('remove', this.dataRemoved);
|
||||
this.telemetryCollection.on('clear', this.dataCleared);
|
||||
this.telemetryCollection.load();
|
||||
},
|
||||
boundsChanged(bounds, isTick) {
|
||||
if (isTick) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bounds = bounds;
|
||||
},
|
||||
timeSystemChanged() {
|
||||
this.timeSystem = this.timeContext.getTimeSystem();
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
this.durationFormatter = this.getFormatter(
|
||||
this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
|
||||
);
|
||||
},
|
||||
normalizeDatum(datum) {
|
||||
const formattedTime = this.formatTime(datum);
|
||||
const time = this.parseTime(formattedTime);
|
||||
|
||||
return {
|
||||
...datum,
|
||||
formattedTime,
|
||||
time
|
||||
};
|
||||
},
|
||||
getFormatter(key) {
|
||||
const metadataValue = this.metadata.value(key) || { format: key };
|
||||
const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
return valueFormatter;
|
||||
}
|
||||
}
|
||||
};
|
31
src/plugins/events/plugin.js
Normal file
31
src/plugins/events/plugin.js
Normal file
@ -0,0 +1,31 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import EventInspectorViewProvider from './EventInspectorViewProvider.js';
|
||||
import EventTimelineViewProvider from './EventTimelineViewProvider.js';
|
||||
|
||||
export default function plugin(extendedLinesBus) {
|
||||
return function install(openmct) {
|
||||
openmct.objectViews.addProvider(new EventTimelineViewProvider(openmct, extendedLinesBus));
|
||||
openmct.inspectorViews.addProvider(new EventInspectorViewProvider(openmct));
|
||||
};
|
||||
}
|
@ -27,9 +27,9 @@ To define a filter, you'll need to add a new `filter` property to the domain obj
|
||||
singleSelectionThreshold: true,
|
||||
comparator: 'equals',
|
||||
possibleValues: [
|
||||
{ name: 'Apple', value: 'apple' },
|
||||
{ name: 'Banana', value: 'banana' },
|
||||
{ name: 'Orange', value: 'orange' }
|
||||
{ label: 'Apple', value: 'apple' },
|
||||
{ label: 'Banana', value: 'banana' },
|
||||
{ label: 'Orange', value: 'orange' }
|
||||
]
|
||||
}]
|
||||
}
|
||||
|
@ -649,6 +649,11 @@ export default {
|
||||
},
|
||||
request(domainObject = this.telemetryObject) {
|
||||
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||
|
||||
if (!this.metadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||
const LimitEvaluator = this.openmct.telemetry.getLimits(domainObject);
|
||||
LimitEvaluator.limits().then(this.updateLimits);
|
||||
|
@ -21,7 +21,7 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div ref="imagery" class="c-imagery-tsv c-timeline-holder">
|
||||
<div ref="imagery" class="c-imagery-tsv js-imagery-tsv" :style="alignmentStyle">
|
||||
<div ref="imageryHolder" class="c-imagery-tsv__contents u-contents"></div>
|
||||
</div>
|
||||
</template>
|
||||
@ -30,24 +30,32 @@
|
||||
import { scaleLinear, scaleUtc } from 'd3-scale';
|
||||
import _ from 'lodash';
|
||||
import mount from 'utils/mount';
|
||||
import { inject } from 'vue';
|
||||
|
||||
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
|
||||
import { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';
|
||||
|
||||
import { useAlignment } from '../../../ui/composables/alignmentContext';
|
||||
import imageryData from '../../imagery/mixins/imageryData.js';
|
||||
|
||||
const AXES_PADDING = 20;
|
||||
const PADDING = 1;
|
||||
const ROW_HEIGHT = 100;
|
||||
const IMAGE_SIZE = 85;
|
||||
const IMAGE_WIDTH_THRESHOLD = 25;
|
||||
const CONTAINER_CLASS = 'c-imagery-tsv-container';
|
||||
const NO_ITEMS_CLASS = 'c-imagery-tsv__no-items';
|
||||
const NO_ITEMS_CLASS = 'c-timeline__no-items';
|
||||
const IMAGE_WRAPPER_CLASS = 'c-imagery-tsv__image-wrapper';
|
||||
const ID_PREFIX = 'wrapper-';
|
||||
|
||||
export default {
|
||||
mixins: [imageryData],
|
||||
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||
setup() {
|
||||
const domainObject = inject('domainObject');
|
||||
const objectPath = inject('objectPath');
|
||||
const openmct = inject('openmct');
|
||||
const { alignment: alignmentData } = useAlignment(domainObject, objectPath, openmct);
|
||||
return { alignmentData };
|
||||
},
|
||||
data() {
|
||||
let timeSystem = this.openmct.time.getTimeSystem();
|
||||
this.metadata = {};
|
||||
@ -62,6 +70,18 @@ export default {
|
||||
keyString: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
alignmentStyle() {
|
||||
let leftOffset = 0;
|
||||
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
|
||||
if (this.alignmentData.leftWidth) {
|
||||
leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
|
||||
}
|
||||
return {
|
||||
margin: `0 ${this.alignmentData.rightWidth + rightOffset}px 0 ${this.alignmentData.leftWidth + leftOffset}px`
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
imageHistory: {
|
||||
handler(newHistory, oldHistory) {
|
||||
@ -73,9 +93,11 @@ export default {
|
||||
mounted() {
|
||||
this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);
|
||||
|
||||
this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
|
||||
this.canvas.height = 0;
|
||||
this.canvasContext = this.canvas.getContext('2d');
|
||||
// Why are we doing this? This element causes scroll problems in the swimlane.
|
||||
// this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
|
||||
// this.canvas.height = 0;
|
||||
// this.canvas.width = 10;
|
||||
// this.canvasContext = this.canvas.getContext('2d');
|
||||
this.setDimensions();
|
||||
|
||||
this.setScaleAndPlotImagery = this.setScaleAndPlotImagery.bind(this);
|
||||
@ -207,8 +229,8 @@ export default {
|
||||
setDimensions() {
|
||||
const imageryHolder = this.$refs.imagery;
|
||||
this.width = this.getClientWidth();
|
||||
|
||||
this.height = Math.round(imageryHolder.getBoundingClientRect().height);
|
||||
this.imageHeight = this.height - 10;
|
||||
},
|
||||
setScale(timeSystem) {
|
||||
if (!this.width) {
|
||||
@ -233,14 +255,11 @@ export default {
|
||||
return imageObj.time <= this.viewBounds.end && imageObj.time >= this.viewBounds.start;
|
||||
},
|
||||
getImageryContainer() {
|
||||
let containerHeight = 100;
|
||||
let containerWidth = this.imageHistory.length ? this.width : 200;
|
||||
let imageryContainer;
|
||||
|
||||
let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
|
||||
if (existingContainer) {
|
||||
imageryContainer = existingContainer;
|
||||
imageryContainer.style.maxWidth = `${containerWidth}px`;
|
||||
} else {
|
||||
if (this.destroyImageryContainer) {
|
||||
this.destroyImageryContainer();
|
||||
@ -270,8 +289,6 @@ export default {
|
||||
this.$refs.imageryHolder.appendChild(component.$el);
|
||||
|
||||
imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
|
||||
imageryContainer.style.maxWidth = `${containerWidth}px`;
|
||||
imageryContainer.style.height = `${containerHeight}px`;
|
||||
}
|
||||
|
||||
return imageryContainer;
|
||||
@ -307,7 +324,7 @@ export default {
|
||||
}
|
||||
},
|
||||
plotNoItems(containerElement) {
|
||||
let textElement = document.createElement('text');
|
||||
let textElement = document.createElement('div');
|
||||
textElement.classList.add(NO_ITEMS_CLASS);
|
||||
textElement.innerHTML = 'No images within timeframe';
|
||||
|
||||
@ -380,15 +397,11 @@ export default {
|
||||
//create image vertical tick indicator
|
||||
let imageTickElement = document.createElement('div');
|
||||
imageTickElement.classList.add('c-imagery-tsv__image-handle');
|
||||
imageTickElement.style.width = '2px';
|
||||
imageTickElement.style.height = `${String(ROW_HEIGHT - 10)}px`;
|
||||
imageWrapper.appendChild(imageTickElement);
|
||||
|
||||
//create placeholder - this will also hold the actual image
|
||||
let imagePlaceholder = document.createElement('div');
|
||||
imagePlaceholder.classList.add('c-imagery-tsv__image-placeholder');
|
||||
imagePlaceholder.style.width = `${IMAGE_SIZE}px`;
|
||||
imagePlaceholder.style.height = `${IMAGE_SIZE}px`;
|
||||
imageWrapper.appendChild(imagePlaceholder);
|
||||
|
||||
//create image element
|
||||
@ -396,8 +409,6 @@ export default {
|
||||
this.setNSAttributesForElement(imageElement, {
|
||||
src: image.thumbnailUrl || image.url
|
||||
});
|
||||
imageElement.style.width = `${IMAGE_SIZE}px`;
|
||||
imageElement.style.height = `${IMAGE_SIZE}px`;
|
||||
this.setImageDisplay(imageElement, showImagePlaceholders);
|
||||
|
||||
//handle mousedown event to show the image in a large view
|
||||
|
@ -509,45 +509,92 @@
|
||||
|
||||
/*************************************** IMAGERY IN TIMESTRIP VIEWS */
|
||||
.c-imagery-tsv {
|
||||
div.c-imagery-tsv__image-wrapper {
|
||||
$m: $interiorMargin;
|
||||
@include abs();
|
||||
// We need overflow: hidden this because an image thumb can extend to the right past the time frame edge
|
||||
overflow: hidden;
|
||||
|
||||
&-container {
|
||||
background: $colorPlotBg;
|
||||
//box-shadow: inset $colorPlotAreaBorder 0 0 0 1px; // Using box-shadow instead of border to not affect box size
|
||||
position: absolute;
|
||||
top: $m; right: 0; bottom: $m; left: 0;
|
||||
}
|
||||
|
||||
.c-imagery-tsv__image-wrapper {
|
||||
$m: $interiorMarginSm;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: $m; bottom: $m;
|
||||
display: flex;
|
||||
z-index: 1;
|
||||
margin-top: 5px;
|
||||
|
||||
img {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
z-index: 2;
|
||||
|
||||
[class*='__image-handle'] {
|
||||
background-color: $colorBodyFg;
|
||||
.c-imagery-tsv {
|
||||
&__image-handle {
|
||||
box-shadow: rgba($colorEventLine, 0.5) 0 0 0px 4px;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&__image-placeholder img {
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
// img can be `display: none` when there's not enough space between tick lines
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__image-placeholder {
|
||||
background-color: deeppink; //pushBack($colorBodyBg, 0.3);
|
||||
$m: $interiorMargin;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: $m; right: auto; bottom: $m; left: 0;
|
||||
|
||||
img {
|
||||
filter: brightness(0.8);
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&__image-handle {
|
||||
$lineW: $eventLineW;
|
||||
$hitAreaW: 7px;
|
||||
background-color: $colorEventLine;
|
||||
transition: box-shadow 250ms ease-out;
|
||||
top: 0; bottom: 0;
|
||||
width: $lineW;
|
||||
z-index: 3;
|
||||
|
||||
&:before {
|
||||
// Extend hit area
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0; bottom: 0;
|
||||
z-index: 0;
|
||||
width: $hitAreaW;
|
||||
transform: translateX(($hitAreaW - $lineW) * -0.5);
|
||||
}
|
||||
}
|
||||
|
||||
&__no-items {
|
||||
fill: $colorBodyFg !important;
|
||||
}
|
||||
|
||||
&__image-handle {
|
||||
background-color: rgba($colorBodyFg, 0.5);
|
||||
}
|
||||
|
||||
&__image-placeholder {
|
||||
background-color: pushBack($colorBodyBg, 0.3);
|
||||
display: block;
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
// DON'T THINK THIS IS BEING USED
|
||||
.c-image-canvas {
|
||||
pointer-events: auto; // This allows the image element to receive a browser-level context click
|
||||
position: absolute;
|
||||
|
@ -25,68 +25,63 @@
|
||||
{{ heading }}
|
||||
</template>
|
||||
<template #object>
|
||||
<svg :height="height" :width="svgWidth" :style="alignmentStyle">
|
||||
<symbol id="activity-bar-bg" :height="rowHeight" width="2" preserveAspectRatio="none">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="currentColor" />
|
||||
<line
|
||||
x1="100%"
|
||||
y1="0"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
stroke="black"
|
||||
stroke-width="1"
|
||||
opacity="0.3"
|
||||
transform="translate(-0.5, 0)"
|
||||
/>
|
||||
</symbol>
|
||||
<template v-for="(activity, index) in activities" :key="`g-${activity.clipPathId}`">
|
||||
<template v-if="clipActivityNames === true">
|
||||
<clipPath :id="activity.clipPathId" :key="activity.clipPathId">
|
||||
<rect
|
||||
<div class="c-plan-av" :style="alignmentStyle">
|
||||
<svg v-if="activities.length > 0" class="c-plan-av__svg" :height="height">
|
||||
<symbol id="activity-bar-bg" :height="rowHeight" width="2" preserveAspectRatio="none">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="currentColor" />
|
||||
<line
|
||||
x1="100%"
|
||||
y1="0"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
stroke="black"
|
||||
stroke-width="1"
|
||||
opacity="0.3"
|
||||
transform="translate(-0.5, 0)"
|
||||
/>
|
||||
</symbol>
|
||||
<template v-for="(activity, index) in activities" :key="`g-${activity.clipPathId}`">
|
||||
<template v-if="clipActivityNames === true">
|
||||
<clipPath :id="activity.clipPathId" :key="activity.clipPathId">
|
||||
<rect
|
||||
:x="activity.rectStart"
|
||||
:y="activity.row"
|
||||
:width="activity.rectWidth - 1"
|
||||
:height="rowHeight"
|
||||
/>
|
||||
</clipPath>
|
||||
</template>
|
||||
<g
|
||||
class="c-plan__activity activity-bounds"
|
||||
@click="setSelectionForActivity(activity, $event)"
|
||||
>
|
||||
<title>{{ activity.name }}</title>
|
||||
<use
|
||||
:key="`rect-${index}`"
|
||||
href="#activity-bar-bg"
|
||||
:x="activity.rectStart"
|
||||
:y="activity.row"
|
||||
:width="activity.rectWidth - 1"
|
||||
:width="activity.rectWidth"
|
||||
:height="rowHeight"
|
||||
:class="activity.class"
|
||||
:color="activity.color"
|
||||
/>
|
||||
</clipPath>
|
||||
<text
|
||||
v-for="(textLine, textIndex) in activity.textLines"
|
||||
:key="`text-${index}-${textIndex}`"
|
||||
:class="`c-plan__activity-label ${activity.textClass}`"
|
||||
:x="activity.textStart"
|
||||
:y="activity.textY + textIndex * lineHeight"
|
||||
:fill="activity.textColor"
|
||||
:clip-path="clipActivityNames === true ? `url(#${activity.clipPathId})` : ''"
|
||||
>
|
||||
{{ textLine }}
|
||||
</text>
|
||||
</g>
|
||||
</template>
|
||||
<g
|
||||
class="c-plan__activity activity-bounds"
|
||||
@click="setSelectionForActivity(activity, $event)"
|
||||
>
|
||||
<title>{{ activity.name }}</title>
|
||||
<use
|
||||
:key="`rect-${index}`"
|
||||
href="#activity-bar-bg"
|
||||
:x="activity.rectStart"
|
||||
:y="activity.row"
|
||||
:width="activity.rectWidth"
|
||||
:height="rowHeight"
|
||||
:class="activity.class"
|
||||
:color="activity.color"
|
||||
/>
|
||||
<text
|
||||
v-for="(textLine, textIndex) in activity.textLines"
|
||||
:key="`text-${index}-${textIndex}`"
|
||||
:class="`c-plan__activity-label ${activity.textClass}`"
|
||||
:x="activity.textStart"
|
||||
:y="activity.textY + textIndex * lineHeight"
|
||||
:fill="activity.textColor"
|
||||
:clip-path="clipActivityNames === true ? `url(#${activity.clipPathId})` : ''"
|
||||
>
|
||||
{{ textLine }}
|
||||
</text>
|
||||
</g>
|
||||
</template>
|
||||
<text
|
||||
v-if="activities.length === 0"
|
||||
x="10"
|
||||
y="20"
|
||||
class="c-plan__activity-label--outside-rect"
|
||||
>
|
||||
No activities within current timeframe
|
||||
</text>
|
||||
</svg>
|
||||
</svg>
|
||||
<div v-else class="c-timeline__no-items">No activities within timeframe</div>
|
||||
</div>
|
||||
</template>
|
||||
</SwimLane>
|
||||
</template>
|
||||
@ -162,24 +157,27 @@ export default {
|
||||
computed: {
|
||||
alignmentStyle() {
|
||||
let leftOffset = 0;
|
||||
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
|
||||
if (this.alignmentData.leftWidth) {
|
||||
leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
|
||||
}
|
||||
return {
|
||||
marginLeft: `${this.alignmentData.leftWidth + leftOffset}px`
|
||||
margin: `0 ${this.alignmentData.rightWidth + rightOffset}px 0 ${this.alignmentData.leftWidth + leftOffset}px`
|
||||
};
|
||||
},
|
||||
svgWidth() {
|
||||
// Reduce the width by left axis width, then take off the right yaxis width as well
|
||||
return '100%'; // TEMP!
|
||||
/*
|
||||
let leftOffset = 0;
|
||||
if (this.alignmentData.leftWidth) {
|
||||
leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
|
||||
}
|
||||
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
|
||||
return (
|
||||
return (
|
||||
this.width -
|
||||
(this.alignmentData.leftWidth + leftOffset + this.alignmentData.rightWidth + rightOffset)
|
||||
);
|
||||
);*/
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -27,6 +27,10 @@
|
||||
text {
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.c-swimlane__lane-object {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&__activity {
|
||||
@ -53,3 +57,14 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.c-plan-av {
|
||||
// Activities view
|
||||
background-color: $colorPlotBg;
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
|
||||
&__svg {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
@ -164,11 +164,13 @@
|
||||
<div
|
||||
v-show="cursorGuide"
|
||||
ref="cursorGuideVertical"
|
||||
aria-label="Vertical cursor guide"
|
||||
class="c-cursor-guide--v js-cursor-guide--v"
|
||||
></div>
|
||||
<div
|
||||
v-show="cursorGuide"
|
||||
ref="cursorGuideHorizontal"
|
||||
aria-label="Horizontal cursor guide"
|
||||
class="c-cursor-guide--h js-cursor-guide--h"
|
||||
></div>
|
||||
</div>
|
||||
@ -537,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);
|
||||
@ -854,13 +857,11 @@ export default {
|
||||
|
||||
this.canvas = this.$refs.chartContainer.querySelectorAll('canvas')[1];
|
||||
|
||||
if (!this.options.compact) {
|
||||
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
|
||||
this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this);
|
||||
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
|
||||
}
|
||||
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
|
||||
this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this);
|
||||
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
|
||||
},
|
||||
|
||||
marqueeAnnotations(annotationsToSelect) {
|
||||
@ -1115,19 +1116,21 @@ export default {
|
||||
this.listenTo(window, 'mouseup', this.onMouseUp, this);
|
||||
this.listenTo(window, 'mousemove', this.trackMousePosition, this);
|
||||
|
||||
// track frozen state on mouseDown to be read on mouseUp
|
||||
const isFrozen =
|
||||
this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
||||
this.isFrozenOnMouseDown = isFrozen;
|
||||
if (!this.options.compact) {
|
||||
// track frozen state on mouseDown to be read on mouseUp
|
||||
const isFrozen =
|
||||
this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
||||
this.isFrozenOnMouseDown = isFrozen;
|
||||
|
||||
if (event.altKey && !event.shiftKey) {
|
||||
return this.startPan(event);
|
||||
} else if (event.altKey && event.shiftKey) {
|
||||
this.freeze();
|
||||
if (event.altKey && !event.shiftKey) {
|
||||
return this.startPan(event);
|
||||
} else if (event.altKey && event.shiftKey) {
|
||||
this.freeze();
|
||||
|
||||
return this.startMarquee(event, true);
|
||||
} else {
|
||||
return this.startMarquee(event, false);
|
||||
return this.startMarquee(event, true);
|
||||
} else {
|
||||
return this.startMarquee(event, false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -1158,11 +1161,15 @@ export default {
|
||||
},
|
||||
|
||||
isMouseClick() {
|
||||
if (!this.marquee) {
|
||||
// We may not have a marquee if we've disabled pan/zoom, but we still need to know if it's a mouse click for highlights and lock points.
|
||||
if (!this.marquee && !this.positionOverPlot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { start, end } = this.marquee;
|
||||
const { start, end } = this.marquee ?? {
|
||||
start: this.positionOverPlot,
|
||||
end: this.positionOverPlot
|
||||
};
|
||||
const someYPositionOverPlot = start.y.some((y) => y);
|
||||
|
||||
return start.x === end.x && someYPositionOverPlot;
|
||||
|
@ -162,14 +162,6 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
gridLines(newGridLines) {
|
||||
this.gridLines = newGridLines;
|
||||
},
|
||||
cursorGuide(newCursorGuide) {
|
||||
this.cursorGuide = newCursorGuide;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
eventHelpers.extend(this);
|
||||
this.imageExporter = new ImageExporter(this.openmct);
|
||||
|
@ -38,6 +38,7 @@ import CouchDBSearchFolder from './CouchDBSearchFolder/plugin.js';
|
||||
import DefaultRootName from './defaultRootName/plugin.js';
|
||||
import DeviceClassifier from './DeviceClassifier/plugin.js';
|
||||
import DisplayLayoutPlugin from './displayLayout/plugin.js';
|
||||
import EventTimestripPlugin from './events/plugin.js';
|
||||
import FaultManagementPlugin from './faultManagement/FaultManagementPlugin.js';
|
||||
import Filters from './filters/plugin.js';
|
||||
import FlexibleLayout from './flexibleLayout/plugin.js';
|
||||
@ -176,5 +177,6 @@ plugins.Gauge = GaugePlugin;
|
||||
plugins.Timelist = TimeList;
|
||||
plugins.InspectorViews = InspectorViews;
|
||||
plugins.InspectorDataVisualization = InspectorDataVisualization;
|
||||
plugins.EventTimestripPlugin = EventTimestripPlugin;
|
||||
|
||||
export default plugins;
|
||||
|
@ -91,15 +91,19 @@ export default class TelemetryTableRow {
|
||||
return [VIEW_DATUM_ACTION_KEY, VIEW_HISTORICAL_DATA_ACTION_KEY];
|
||||
}
|
||||
|
||||
updateWithDatum(updatesToDatum) {
|
||||
const normalizedUpdatesToDatum = createNormalizedDatum(updatesToDatum, this.columns);
|
||||
/**
|
||||
* Merges the row parameter's datum with the current row datum
|
||||
* @param {TelemetryTableRow} row
|
||||
*/
|
||||
updateWithDatum(row) {
|
||||
this.datum = {
|
||||
...this.datum,
|
||||
...normalizedUpdatesToDatum
|
||||
...row.datum
|
||||
};
|
||||
|
||||
this.fullDatum = {
|
||||
...this.fullDatum,
|
||||
...updatesToDatum
|
||||
...row.fullDatum
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,11 @@ import { EventEmitter } from 'eventemitter3';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { ORDER } from '../constants.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('.TelemetryTableRow.js').default} TelemetryTableRow
|
||||
*/
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
@ -124,10 +129,22 @@ export default class TableRowCollection extends EventEmitter {
|
||||
return foundIndex;
|
||||
}
|
||||
|
||||
updateRowInPlace(row, index) {
|
||||
const foundRow = this.rows[index];
|
||||
foundRow.updateWithDatum(row.datum);
|
||||
this.rows[index] = foundRow;
|
||||
/**
|
||||
* `incomingRow` exists in the collection,
|
||||
* so merge existing and incoming row properties
|
||||
*
|
||||
* Do to reactivity of Vue, we want to replace the existing row with the updated row
|
||||
* @param {TelemetryTableRow} incomingRow to update
|
||||
* @param {number} index of the existing row in the collection to update
|
||||
*/
|
||||
updateRowInPlace(incomingRow, index) {
|
||||
// Update the incoming row, not the existing row
|
||||
const existingRow = this.rows[index];
|
||||
incomingRow.updateWithDatum(existingRow);
|
||||
|
||||
// Replacing the existing row with the updated, incoming row will trigger Vue reactivity
|
||||
// because the reference to the row has changed
|
||||
this.rows.splice(index, 1, incomingRow);
|
||||
}
|
||||
|
||||
setLimit(rowLimit) {
|
||||
|
@ -373,7 +373,6 @@ export default {
|
||||
configuredColumnWidths: configuration.columnWidths,
|
||||
sizingRows: {},
|
||||
rowHeight: ROW_HEIGHT,
|
||||
scrollOffset: 0,
|
||||
totalHeight: 0,
|
||||
totalWidth: 0,
|
||||
rowOffset: 0,
|
||||
@ -552,6 +551,7 @@ export default {
|
||||
//Default sort
|
||||
this.sortOptions = this.table.tableRows.sortBy();
|
||||
this.scrollable = this.$refs.scrollable;
|
||||
this.lastScrollLeft = this.scrollable.scrollLeft;
|
||||
this.contentTable = this.$refs.contentTable;
|
||||
this.sizingTable = this.$refs.sizingTable;
|
||||
this.headersHolderEl = this.$refs.headersHolderEl;
|
||||
@ -740,7 +740,9 @@ export default {
|
||||
this.table.sortBy(this.sortOptions);
|
||||
},
|
||||
scroll() {
|
||||
this.throttledUpdateVisibleRows();
|
||||
if (this.lastScrollLeft === this.scrollable.scrollLeft) {
|
||||
this.throttledUpdateVisibleRows();
|
||||
}
|
||||
this.synchronizeScrollX();
|
||||
|
||||
if (this.shouldAutoScroll()) {
|
||||
@ -765,6 +767,8 @@ export default {
|
||||
this.scrollable.scrollTop = Number.MAX_SAFE_INTEGER;
|
||||
},
|
||||
synchronizeScrollX() {
|
||||
this.lastScrollLeft = this.scrollable.scrollLeft;
|
||||
|
||||
if (this.$refs.headersHolderEl && this.scrollable) {
|
||||
this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft;
|
||||
}
|
||||
|
@ -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 =
|
||||
|
37
src/plugins/timeline/ExtendedLinesBus.js
Normal file
37
src/plugins/timeline/ExtendedLinesBus.js
Normal file
@ -0,0 +1,37 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
export default class ExtendedLinesBus extends EventEmitter {
|
||||
updateExtendedLines(keyString, lines) {
|
||||
this.emit('update-extended-lines', { lines, keyString });
|
||||
}
|
||||
disableExtendEventLines(keyString) {
|
||||
this.emit('disable-extended-lines', keyString);
|
||||
}
|
||||
enableExtendEventLines(keyString) {
|
||||
this.emit('enable-extended-lines', keyString);
|
||||
}
|
||||
updateHoverExtendEventLine(keyString, id) {
|
||||
this.emit('update-extended-hover', { id, keyString });
|
||||
}
|
||||
}
|
88
src/plugins/timeline/ExtendedLinesOverlay.vue
Normal file
88
src/plugins/timeline/ExtendedLinesOverlay.vue
Normal file
@ -0,0 +1,88 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<div class="c-timeline__overlay-lines">
|
||||
<div
|
||||
v-for="(lines, key) in extendedLinesPerKey"
|
||||
:key="key"
|
||||
class="c-timeline__event-line--extended-container"
|
||||
>
|
||||
<div
|
||||
v-for="(line, index) in lines"
|
||||
:id="line.id"
|
||||
:key="`${index - line.id}`"
|
||||
class="c-timeline__event-line--extended"
|
||||
:class="[
|
||||
line.limitClass,
|
||||
{
|
||||
'--hilite':
|
||||
(hoveredLineId && hoveredKeyString === key && line.id === hoveredLineId) ||
|
||||
(selectedLineId && selectedKeyString === key && line.id === selectedLineId)
|
||||
}
|
||||
]"
|
||||
:style="{ left: `${line.x + leftOffset}px`, height: `${height}px` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ExtendedLinesOverlay',
|
||||
props: {
|
||||
extendedLinesPerKey: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
leftOffset: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
extendedLineHover: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
extendedLineSelection: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hoveredLineId() {
|
||||
return this.extendedLineHover.id || null;
|
||||
},
|
||||
hoveredKeyString() {
|
||||
return this.extendedLineHover.keyString || null;
|
||||
},
|
||||
selectedLineId() {
|
||||
return this.extendedLineSelection.id || null;
|
||||
},
|
||||
selectedKeyString() {
|
||||
return this.extendedLineSelection.keyString || null;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -33,10 +33,7 @@ export default function TimelineCompositionPolicy(openmct) {
|
||||
}
|
||||
|
||||
function hasDomainAndRange(metadata) {
|
||||
return (
|
||||
metadata.valuesForHints(['range']).length > 0 &&
|
||||
metadata.valuesForHints(['domain']).length > 0
|
||||
);
|
||||
return metadata.valuesForHints(['domain']).length > 0;
|
||||
}
|
||||
|
||||
function hasImageTelemetry(domainObject, metadata) {
|
||||
|
@ -28,6 +28,11 @@
|
||||
:show-ucontents="isPlanLikeObject(item.domainObject)"
|
||||
:span-rows-count="item.rowCount"
|
||||
:domain-object="item.domainObject"
|
||||
:button-title="`Toggle extended event lines overlay for ${item.domainObject.name}`"
|
||||
button-icon="icon-arrows-up-down"
|
||||
:hide-button="!item.isEventTelemetry"
|
||||
:button-click-on="enableExtendEventLines"
|
||||
:button-click-off="disableExtendEventLines"
|
||||
>
|
||||
<template #label>
|
||||
{{ item.domainObject.name }}
|
||||
@ -58,12 +63,16 @@ export default {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
extendedLinesBus: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
domainObject: undefined,
|
||||
mutablePromise: undefined,
|
||||
domainObject: null,
|
||||
mutablePromise: null,
|
||||
status: ''
|
||||
};
|
||||
},
|
||||
@ -103,33 +112,40 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setObject(domainObject) {
|
||||
async setObject(domainObject) {
|
||||
this.domainObject = domainObject;
|
||||
this.mutablePromise = undefined;
|
||||
this.$nextTick(() => {
|
||||
let reference = this.$refs.objectView;
|
||||
this.mutablePromise = null;
|
||||
await this.$nextTick();
|
||||
let reference = this.$refs.objectView;
|
||||
|
||||
if (reference) {
|
||||
let childContext = this.$refs.objectView.getSelectionContext();
|
||||
childContext.item = domainObject;
|
||||
this.context = childContext;
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
|
||||
this.removeSelectable = this.openmct.selection.selectable(this.$el, this.context);
|
||||
if (reference) {
|
||||
let childContext = this.$refs.objectView.getSelectionContext();
|
||||
childContext.item = domainObject;
|
||||
this.context = childContext;
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
|
||||
if (this.removeStatusListener) {
|
||||
this.removeStatusListener();
|
||||
}
|
||||
this.removeSelectable = this.openmct.selection.selectable(this.$el, this.context);
|
||||
}
|
||||
|
||||
this.removeStatusListener = this.openmct.status.observe(
|
||||
this.domainObject.identifier,
|
||||
this.setStatus
|
||||
);
|
||||
this.status = this.openmct.status.get(this.domainObject.identifier);
|
||||
});
|
||||
if (this.removeStatusListener) {
|
||||
this.removeStatusListener();
|
||||
}
|
||||
|
||||
this.removeStatusListener = this.openmct.status.observe(
|
||||
this.domainObject.identifier,
|
||||
this.setStatus
|
||||
);
|
||||
this.status = this.openmct.status.get(this.domainObject.identifier);
|
||||
},
|
||||
enableExtendEventLines() {
|
||||
const keyString = this.openmct.objects.makeKeyString(this.item.domainObject.identifier);
|
||||
this.extendedLinesBus.enableExtendEventLines(keyString);
|
||||
},
|
||||
disableExtendEventLines() {
|
||||
const keyString = this.openmct.objects.makeKeyString(this.item.domainObject.identifier);
|
||||
this.extendedLinesBus.disableExtendEventLines(keyString);
|
||||
},
|
||||
setActionCollection(actionCollection) {
|
||||
this.openmct.menus.actionsToMenuItems(
|
||||
|
@ -42,8 +42,17 @@
|
||||
:key="item.keyString"
|
||||
class="c-timeline__content js-timeline__content"
|
||||
:item="item"
|
||||
:extended-lines-bus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ExtendedLinesOverlay
|
||||
:extended-lines-per-key="extendedLinesPerKey"
|
||||
:height="height"
|
||||
:left-offset="extendedLinesLeftOffset"
|
||||
:extended-line-hover="extendedLineHover"
|
||||
:extended-line-selection="extendedLineSelection"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -56,6 +65,7 @@ import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
|
||||
import TimelineAxis from '../../ui/components/TimeSystemAxis.vue';
|
||||
import { useAlignment } from '../../ui/composables/alignmentContext.js';
|
||||
import { getValidatedData, getValidatedGroups } from '../plan/util.js';
|
||||
import ExtendedLinesOverlay from './ExtendedLinesOverlay.vue';
|
||||
import TimelineObjectView from './TimelineObjectView.vue';
|
||||
|
||||
const unknownObjectType = {
|
||||
@ -65,13 +75,17 @@ const unknownObjectType = {
|
||||
}
|
||||
};
|
||||
|
||||
const AXES_PADDING = 20;
|
||||
const PLOT_ITEM_H_PX = 100;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TimelineObjectView,
|
||||
TimelineAxis,
|
||||
SwimLane
|
||||
SwimLane,
|
||||
ExtendedLinesOverlay
|
||||
},
|
||||
inject: ['openmct', 'domainObject', 'path', 'composition'],
|
||||
inject: ['openmct', 'domainObject', 'path', 'composition', 'extendedLinesBus'],
|
||||
setup() {
|
||||
const domainObject = inject('domainObject');
|
||||
const path = inject('path');
|
||||
@ -90,9 +104,21 @@ export default {
|
||||
timeSystems: [],
|
||||
height: 0,
|
||||
useIndependentTime: this.domainObject.configuration.useIndependentTime === true,
|
||||
timeOptions: this.domainObject.configuration.timeOptions
|
||||
timeOptions: this.domainObject.configuration.timeOptions,
|
||||
extendedLinesPerKey: {},
|
||||
extendedLineHover: {},
|
||||
extendedLineSelection: {},
|
||||
extendedLinesLeftOffset: 0
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
alignmentData: {
|
||||
handler() {
|
||||
this.calculateExtendedLinesLeftOffset();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.resetAlignment();
|
||||
this.composition.off('add', this.addItem);
|
||||
@ -101,11 +127,18 @@ export default {
|
||||
this.stopFollowingTimeContext();
|
||||
this.handleContentResize.cancel();
|
||||
this.contentResizeObserver.disconnect();
|
||||
this.extendedLinesBus.off('update-extended-lines', this.updateExtendedLines);
|
||||
this.extendedLinesBus.off('update-extended-hover', this.updateExtendedHover);
|
||||
this.openmct.selection.off('change', this.checkForLineSelection);
|
||||
},
|
||||
mounted() {
|
||||
this.items = [];
|
||||
this.setTimeContext();
|
||||
|
||||
this.extendedLinesBus.on('update-extended-lines', this.updateExtendedLines);
|
||||
this.extendedLinesBus.on('update-extended-hover', this.updateExtendedHover);
|
||||
this.openmct.selection.on('change', this.checkForLineSelection);
|
||||
|
||||
if (this.composition) {
|
||||
this.composition.on('add', this.addItem);
|
||||
this.composition.on('remove', this.removeItem);
|
||||
@ -129,27 +162,41 @@ export default {
|
||||
} else if (domainObject.type === 'gantt-chart') {
|
||||
rowCount = Object.keys(domainObject.configuration.swimlaneVisibility).length;
|
||||
}
|
||||
|
||||
const isEventTelemetry = this.hasEventTelemetry(domainObject);
|
||||
let height =
|
||||
domainObject.type === 'telemetry.plot.stacked'
|
||||
? `${domainObject.composition.length * 100}px`
|
||||
: '100px';
|
||||
? `${domainObject.composition.length * PLOT_ITEM_H_PX}px`
|
||||
: 'auto';
|
||||
let item = {
|
||||
domainObject,
|
||||
objectPath,
|
||||
type,
|
||||
keyString,
|
||||
rowCount,
|
||||
height
|
||||
height,
|
||||
isEventTelemetry
|
||||
};
|
||||
|
||||
this.items.push(item);
|
||||
},
|
||||
hasEventTelemetry(domainObject) {
|
||||
const metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||
if (!metadata) {
|
||||
return false;
|
||||
}
|
||||
const hasDomain = metadata.valuesForHints(['domain']).length > 0;
|
||||
const hasNoRange = !metadata.valuesForHints(['range'])?.length;
|
||||
// for the moment, let's also exclude telemetry with images
|
||||
const hasNoImages = !metadata.valuesForHints(['image']).length;
|
||||
|
||||
return hasDomain && hasNoRange && hasNoImages;
|
||||
},
|
||||
removeItem(identifier) {
|
||||
let index = this.items.findIndex((item) =>
|
||||
this.openmct.objects.areIdsEqual(identifier, item.domainObject.identifier)
|
||||
);
|
||||
this.items.splice(index, 1);
|
||||
delete this.extendedLinesPerKey[this.openmct.objects.makeKeyString(identifier)];
|
||||
},
|
||||
reorder(reorderPlan) {
|
||||
let oldItems = this.items.slice();
|
||||
@ -165,6 +212,7 @@ export default {
|
||||
if (this.height !== clientHeight) {
|
||||
this.height = clientHeight;
|
||||
}
|
||||
this.calculateExtendedLinesLeftOffset();
|
||||
},
|
||||
getClientHeight() {
|
||||
let clientHeight = this.$refs.timelineHolder.getBoundingClientRect().height;
|
||||
@ -222,6 +270,41 @@ export default {
|
||||
this.timeContext.off('boundsChanged', this.updateViewBounds);
|
||||
this.timeContext.off('clockChanged', this.updateViewBounds);
|
||||
}
|
||||
},
|
||||
updateExtendedLines({ keyString, lines }) {
|
||||
this.extendedLinesPerKey[keyString] = lines;
|
||||
},
|
||||
updateExtendedHover({ keyString, id }) {
|
||||
this.extendedLineHover = { keyString, id };
|
||||
},
|
||||
checkForLineSelection(selection) {
|
||||
const selectionContext = selection?.[0]?.[0]?.context;
|
||||
const eventType = selectionContext?.type;
|
||||
if (eventType === 'time-strip-event-selection') {
|
||||
const event = selectionContext.event;
|
||||
const selectedObject = selectionContext.item;
|
||||
const keyString = this.openmct.objects.makeKeyString(selectedObject.identifier);
|
||||
this.extendedLineSelection = { keyString, id: event?.time };
|
||||
} else {
|
||||
this.extendedLineSelection = {};
|
||||
}
|
||||
},
|
||||
calculateExtendedLinesLeftOffset() {
|
||||
const swimLaneOffset = this.calculateSwimlaneOffset();
|
||||
this.extendedLinesLeftOffset = this.alignmentData.leftWidth + swimLaneOffset;
|
||||
},
|
||||
calculateSwimlaneOffset() {
|
||||
const firstSwimLane = this.$el.querySelector('.c-swimlane__lane-object');
|
||||
if (firstSwimLane) {
|
||||
const timelineHolderRect = this.$refs.timelineHolder.getBoundingClientRect();
|
||||
const laneObjectRect = firstSwimLane.getBoundingClientRect();
|
||||
const offset = laneObjectRect.left - timelineHolderRect.left;
|
||||
const hasAxes = this.alignmentData.axes && Object.keys(this.alignmentData.axes).length > 0;
|
||||
const swimLaneOffset = hasAxes ? offset + AXES_PADDING : offset;
|
||||
return swimLaneOffset;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -24,7 +24,7 @@ import mount from 'utils/mount';
|
||||
|
||||
import TimelineViewLayout from './TimelineViewLayout.vue';
|
||||
|
||||
export default function TimelineViewProvider(openmct) {
|
||||
export default function TimelineViewProvider(openmct, extendedLinesBus) {
|
||||
return {
|
||||
key: 'time-strip.view',
|
||||
name: 'TimeStrip',
|
||||
@ -52,7 +52,8 @@ export default function TimelineViewProvider(openmct) {
|
||||
openmct,
|
||||
domainObject,
|
||||
path: objectPath,
|
||||
composition: openmct.composition.get(domainObject)
|
||||
composition: openmct.composition.get(domainObject),
|
||||
extendedLinesBus
|
||||
},
|
||||
template: '<timeline-view-layout></timeline-view-layout>'
|
||||
},
|
||||
|
@ -20,12 +20,17 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import ExtendedLinesBus from './ExtendedLinesBus.js';
|
||||
import TimelineCompositionPolicy from './TimelineCompositionPolicy.js';
|
||||
import timelineInterceptor from './timelineInterceptor.js';
|
||||
import TimelineViewProvider from './TimelineViewProvider.js';
|
||||
|
||||
const extendedLinesBus = new ExtendedLinesBus();
|
||||
|
||||
export { extendedLinesBus };
|
||||
|
||||
export default function () {
|
||||
return function install(openmct) {
|
||||
function install(openmct) {
|
||||
openmct.types.addType('time-strip', {
|
||||
name: 'Time Strip',
|
||||
key: 'time-strip',
|
||||
@ -43,6 +48,10 @@ export default function () {
|
||||
timelineInterceptor(openmct);
|
||||
openmct.composition.addPolicy(new TimelineCompositionPolicy(openmct).allow);
|
||||
|
||||
openmct.objectViews.addProvider(new TimelineViewProvider(openmct));
|
||||
};
|
||||
openmct.objectViews.addProvider(new TimelineViewProvider(openmct, extendedLinesBus));
|
||||
}
|
||||
|
||||
install.extendedLinesBus = extendedLinesBus;
|
||||
|
||||
return install;
|
||||
}
|
||||
|
@ -1,7 +1,62 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/********************************************* TIME STRIP */
|
||||
.c-timeline-holder {
|
||||
overflow: hidden;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
|
||||
// Plot view overrides
|
||||
.gl-plot-display-area,
|
||||
.gl-plot-axis-area.gl-plot-y {
|
||||
bottom: $interiorMargin !important;
|
||||
}
|
||||
}
|
||||
|
||||
.c-timeline__objects {
|
||||
display: contents;
|
||||
.c-timeline {
|
||||
&__objects {
|
||||
display: contents;
|
||||
|
||||
.c-swimlane {
|
||||
min-height: 100px; // TEMP!! Will be replaced when heights are set by user
|
||||
}
|
||||
}
|
||||
|
||||
&__overlay-lines {
|
||||
//background: rgba(deeppink, 0.2);
|
||||
@include abs();
|
||||
top: 20px; // Offset down to line up with time axis ticks line
|
||||
pointer-events: none; // Allows clicks to pass through
|
||||
z-index: 10; // Ensure it sits atop swimlanes
|
||||
}
|
||||
|
||||
&__no-items {
|
||||
font-style: italic;
|
||||
position: absolute;
|
||||
left: $interiorMargin;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
@ -436,6 +436,9 @@ export default {
|
||||
|
||||
return startInBounds || endInBounds || middleInBounds;
|
||||
},
|
||||
isActivityInProgress(activity) {
|
||||
return this.persistedActivityStates[activity.id] === 'in-progress';
|
||||
},
|
||||
filterActivities(activity) {
|
||||
if (this.isEditing) {
|
||||
return true;
|
||||
@ -460,7 +463,8 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.isActivityInBounds(activity)) {
|
||||
// An activity may be out of bounds, but if it is in-progress, we show it.
|
||||
if (!this.isActivityInBounds(activity) && !this.isActivityInProgress(activity)) {
|
||||
return false;
|
||||
}
|
||||
//current event or future start event or past end event
|
||||
|
@ -443,6 +443,10 @@ $colorEventPurpleBg: #31204a;
|
||||
$colorEventRedBg: #3c1616;
|
||||
$colorEventOrangeBg: #3e2a13;
|
||||
$colorEventYellowBg: #3e3316;
|
||||
$colorEventPurpleLine: #9e36ff;
|
||||
$colorEventRedLine: #ff2525;
|
||||
$colorEventOrangeLine: #ff8800;
|
||||
$colorEventYellowLine: #fdce22;
|
||||
|
||||
// Bubble colors
|
||||
$colorInfoBubbleBg: #dddddd;
|
||||
@ -517,6 +521,11 @@ $colorInProgressBg: $colorTimeRealtimeBg;
|
||||
$colorInProgressFg: $colorTimeRealtimeFgSubtle;
|
||||
$colorInProgressFgEm: $colorTimeRealtimeFg;
|
||||
$colorGanttSelectedBorder: rgba(#fff, 0.3);
|
||||
$colorEventLine: $colorBodyFg;
|
||||
$colorEventLineExtended: rgba($colorEventLine, 0.3);
|
||||
$colorTimeStripDraftBg: rgba(#a57748, 0.2);
|
||||
$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);
|
||||
$eventLineW: 1px;
|
||||
|
||||
// Tree
|
||||
$colorTreeBg: transparent;
|
||||
|
@ -412,6 +412,10 @@ $colorEventPurpleBg: #31204a;
|
||||
$colorEventRedBg: #3c1616;
|
||||
$colorEventOrangeBg: #3e2a13;
|
||||
$colorEventYellowBg: #3e3316;
|
||||
$colorEventPurpleLine: #9e36ff;
|
||||
$colorEventRedLine: #ff2525;
|
||||
$colorEventOrangeLine: #ff8800;
|
||||
$colorEventYellowLine: #fdce22;
|
||||
|
||||
// Bubble colors
|
||||
$colorInfoBubbleBg: #dddddd;
|
||||
@ -482,6 +486,11 @@ $colorInProgressBg: $colorTimeRealtimeBg;
|
||||
$colorInProgressFg: $colorTimeRealtimeFgSubtle;
|
||||
$colorInProgressFgEm: $colorTimeRealtimeFg;
|
||||
$colorGanttSelectedBorder: rgba(#fff, 0.3);
|
||||
$colorEventLine: $colorBodyFg;
|
||||
$colorEventLineExtended: rgba($colorEventLine, 0.3);
|
||||
$colorTimeStripDraftBg: rgba(#a57748, 0.2);
|
||||
$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);
|
||||
$eventLineW: 1px;
|
||||
|
||||
// Tree
|
||||
$colorTreeBg: transparent;
|
||||
|
@ -428,6 +428,10 @@ $colorEventPurpleBg: #31204a;
|
||||
$colorEventRedBg: #3c1616;
|
||||
$colorEventOrangeBg: #3e2a13;
|
||||
$colorEventYellowBg: #3e3316;
|
||||
$colorEventPurpleLine: #9e36ff;
|
||||
$colorEventRedLine: #ff2525;
|
||||
$colorEventOrangeLine: #ff8800;
|
||||
$colorEventYellowLine: #fdce22;
|
||||
|
||||
// Bubble colors
|
||||
$colorInfoBubbleBg: #dddddd;
|
||||
@ -498,6 +502,11 @@ $colorInProgressBg: $colorTimeRealtimeBg;
|
||||
$colorInProgressFg: $colorTimeRealtimeFgSubtle;
|
||||
$colorInProgressFgEm: $colorTimeRealtimeFg;
|
||||
$colorGanttSelectedBorder: rgba(#fff, 0.3);
|
||||
$colorEventLine: $colorBodyFg;
|
||||
$colorEventLineExtended: rgba($colorEventLine, 0.3);
|
||||
$colorTimeStripDraftBg: rgba(#a57748, 0.2);
|
||||
$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);
|
||||
$eventLineW: 1px;
|
||||
|
||||
// Tree
|
||||
$colorTreeBg: transparent;
|
||||
|
@ -403,14 +403,18 @@ $colorLimitCyanFg: #d3faff;
|
||||
$colorLimitCyanIc: #1795c0;
|
||||
|
||||
// Events
|
||||
$colorEventPurpleFg: #6433ff;
|
||||
$colorEventPurpleFg: #6f07ed;
|
||||
$colorEventRedFg: #aa0000;
|
||||
$colorEventOrangeFg: #b84900;
|
||||
$colorEventYellowFg: #867109;
|
||||
$colorEventYellowFg: #a98c04;
|
||||
$colorEventPurpleBg: #ebe7fb;
|
||||
$colorEventRedBg: #fcefef;
|
||||
$colorEventOrangeBg: #ffece3;
|
||||
$colorEventYellowBg: #fdf8eb;
|
||||
$colorEventPurpleLine: $colorEventPurpleFg;
|
||||
$colorEventRedLine: $colorEventRedFg;
|
||||
$colorEventOrangeLine: $colorEventOrangeFg;
|
||||
$colorEventYellowLine: $colorEventYellowFg;
|
||||
|
||||
// Bubble colors
|
||||
$colorInfoBubbleBg: $colorMenuBg;
|
||||
@ -481,6 +485,11 @@ $colorInProgressBg: #b1e8ff;
|
||||
$colorInProgressFg: $colorCurrentFg;
|
||||
$colorInProgressFgEm: $colorCurrentFgEm;
|
||||
$colorGanttSelectedBorder: #fff;
|
||||
$colorEventLine: $colorBodyFg;
|
||||
$colorEventLineExtended: rgba($colorEventLine, 0.3);
|
||||
$colorTimeStripDraftBg: rgba(#a57748, 0.2);
|
||||
$colorTimeStripLabelBg: rgba($colorBodyFg, 0.15);
|
||||
$eventLineW: 1px;
|
||||
|
||||
// Tree
|
||||
$colorTreeBg: transparent;
|
||||
|
@ -252,8 +252,6 @@ tr {
|
||||
background-color: $colorEventYellowBg !important;
|
||||
color: $colorEventYellowFg !important;
|
||||
}
|
||||
&--no-style {
|
||||
background-color: $colorBodyBg !important;
|
||||
color: $colorBodyFg !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -11,6 +11,7 @@
|
||||
@import '../plugins/displayLayout/components/layout-frame.scss';
|
||||
@import '../plugins/displayLayout/components/telemetry-view.scss';
|
||||
@import '../plugins/displayLayout/components/text-view.scss';
|
||||
@import '../plugins/events/components/events-view.scss';
|
||||
@import '../plugins/filters/components/filters-view.scss';
|
||||
@import '../plugins/filters/components/global-filters.scss';
|
||||
@import '../plugins/flexibleLayout/components/flexible-layout.scss';
|
||||
|
@ -21,9 +21,9 @@
|
||||
-->
|
||||
<template>
|
||||
<div ref="axisHolder" class="c-timesystem-axis">
|
||||
<div class="nowMarker" :style="nowMarkerStyle"><span class="icon-arrow-down"></span></div>
|
||||
<div class="c-timesystem-axis__mb-line" :style="nowMarkerStyle" aria-label="Now Marker"></div>
|
||||
<svg :width="svgWidth" :height="svgHeight">
|
||||
<g class="axis" font-size="1.3em" :transform="axisTransform"></g>
|
||||
<g class="axis" :transform="axisTransform"></g>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
@ -44,6 +44,7 @@ import { useResizeObserver } from '../composables/resize';
|
||||
const PADDING = 1;
|
||||
const PIXELS_PER_TICK = 100;
|
||||
const PIXELS_PER_TICK_WIDE = 200;
|
||||
const TIME_AXIS_LINE_Y = 20;
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
@ -78,8 +79,9 @@ export default {
|
||||
const { size: containerSize, startObserving } = useResizeObserver();
|
||||
const svgWidth = ref(0);
|
||||
const svgHeight = ref(0);
|
||||
const axisTransform = ref('translate(0,20)');
|
||||
const axisTransform = ref(`translate(0,${TIME_AXIS_LINE_Y})`);
|
||||
const alignmentOffset = ref(0);
|
||||
const alignmentStyle = ref({ margin: `0 0 0 0` });
|
||||
const nowMarkerStyle = reactive({
|
||||
height: '0px',
|
||||
left: '0px'
|
||||
@ -102,6 +104,7 @@ export default {
|
||||
svgHeight,
|
||||
axisTransform,
|
||||
alignmentOffset,
|
||||
alignmentStyle,
|
||||
nowMarkerStyle,
|
||||
openmct
|
||||
};
|
||||
@ -110,14 +113,17 @@ export default {
|
||||
alignmentData: {
|
||||
handler() {
|
||||
let leftOffset = 0;
|
||||
const rightOffset = this.alignmentData.rightWidth ? AXES_PADDING : 0;
|
||||
if (this.alignmentData.leftWidth) {
|
||||
leftOffset = this.alignmentData.multiple ? 2 * AXES_PADDING : AXES_PADDING;
|
||||
}
|
||||
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.alignmentStyle = {
|
||||
margin: `0 ${this.leftAlignmentOffset + this.alignmentData.rightWidth + rightOffset}px 0 ${this.alignmentData.leftWidth + leftOffset}px`
|
||||
};
|
||||
this.refresh();
|
||||
},
|
||||
deep: true
|
||||
@ -169,14 +175,15 @@ export default {
|
||||
this.updateNowMarker();
|
||||
},
|
||||
updateNowMarker() {
|
||||
const nowMarker = this.$el.querySelector('.nowMarker');
|
||||
const nowMarker = this.$el.querySelector('.c-timesystem-axis__mb-line');
|
||||
if (nowMarker) {
|
||||
nowMarker.classList.remove('hidden');
|
||||
this.nowMarkerStyle.height = this.contentHeight + 'px';
|
||||
this.nowMarkerStyle.height = this.contentHeight - TIME_AXIS_LINE_Y + 'px';
|
||||
this.nowMarkerStyle.top = TIME_AXIS_LINE_Y + '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');
|
||||
}
|
||||
}
|
||||
|
@ -42,10 +42,19 @@
|
||||
:title="`This item is ${status}`"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div class="c-object-label__name">
|
||||
<slot name="label"></slot>
|
||||
</div>
|
||||
<div class="c-swimlane__lane-label-button-h">
|
||||
<button
|
||||
v-if="!hideButton"
|
||||
class="c-button"
|
||||
:class="[buttonIcon, buttonPressed ? 'is-active' : '']"
|
||||
:title="buttonTitle"
|
||||
:aria-label="buttonTitle"
|
||||
@click="pressOnButton"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c-swimlane__lane-object"
|
||||
@ -115,8 +124,43 @@ export default {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
hideButton: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
buttonTitle: {
|
||||
type: String,
|
||||
default() {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
buttonIcon: {
|
||||
type: String,
|
||||
default() {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
buttonClickOn: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {};
|
||||
}
|
||||
},
|
||||
buttonClickOff: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {};
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
buttonPressed: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
gridRowSpan() {
|
||||
if (this.spanRowsCount) {
|
||||
@ -128,7 +172,7 @@ export default {
|
||||
|
||||
swimlaneClass() {
|
||||
if (!this.spanRowsCount && !this.isNested) {
|
||||
return 'c-swimlane__lane-label--span-cols';
|
||||
return 'c-swimlane__lane-label --span-cols';
|
||||
}
|
||||
|
||||
return '';
|
||||
@ -142,6 +186,14 @@ export default {
|
||||
async showToolTip() {
|
||||
const { BELOW } = this.openmct.tooltips.TOOLTIP_LOCATIONS;
|
||||
this.buildToolTip(await this.getObjectPath(), BELOW, 'swimLane');
|
||||
},
|
||||
pressOnButton() {
|
||||
this.buttonPressed = !this.buttonPressed;
|
||||
if (this.buttonPressed) {
|
||||
this.buttonClickOn();
|
||||
} else {
|
||||
this.buttonClickOff();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -24,26 +24,22 @@
|
||||
display: grid;
|
||||
grid-template-columns: 100px 100px 1fr;
|
||||
grid-column-gap: 1px;
|
||||
grid-row-gap: 1px;
|
||||
margin-bottom: 1px;
|
||||
grid-row-gap: 1px; // Used for grid within a swimlane for Plan views
|
||||
width: 100%;
|
||||
|
||||
&.is-status--draft {
|
||||
background: rgba($colorAlert, 0.2);
|
||||
background: $colorTimeStripDraftBg;
|
||||
}
|
||||
|
||||
[class*='__lane-label'] {
|
||||
background: rgba($colorBodyFg, 0.2);
|
||||
&__lane-label {
|
||||
background: $colorTimeStripLabelBg;
|
||||
color: $colorBodyFg;
|
||||
padding: $interiorMarginSm;
|
||||
}
|
||||
|
||||
[class*='--span-cols'] {
|
||||
grid-column: span 2;
|
||||
padding: $interiorMarginSm $interiorMargin;
|
||||
}
|
||||
|
||||
&__lane-object {
|
||||
background: rgba(black, 0.1);
|
||||
height: 100%;
|
||||
|
||||
.c-plan {
|
||||
display: contents;
|
||||
@ -52,10 +48,18 @@
|
||||
@include smallerControlButtons;
|
||||
}
|
||||
|
||||
// Yet more brittle special case selecting...
|
||||
.is-object-type-plan {
|
||||
display: contents;
|
||||
&__lane-label-button-h {
|
||||
// Holds swimlane button(s)
|
||||
flex: 1 1 auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.--span-cols {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
// Yet more brittle special case selecting...
|
||||
.is-object-type-plan,
|
||||
.is-object-type-gantt-chart {
|
||||
display: contents;
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
@use 'sass:math';
|
||||
|
||||
.c-timesystem-axis {
|
||||
$h: 30px;
|
||||
height: $h;
|
||||
|
||||
svg {
|
||||
$lineC: rgba($colorBodyFg, 0.3) !important;
|
||||
$lineC: $colorInteriorBorder; //rgba($colorBodyFg, 0.3) !important;
|
||||
text-rendering: geometricPrecision;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -26,21 +28,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
.nowMarker {
|
||||
width: 2px;
|
||||
&__mb-line {
|
||||
$c: $colorTimeRealtimeBtnBgMajor;
|
||||
$w: 13px;
|
||||
$wHalf: math.floor(math.div($w, 2));
|
||||
//$h: 5px;
|
||||
//$hHalf: math.floor(math.div($h, 2));
|
||||
$transform: translateX(($wHalf - 1) * -1);
|
||||
|
||||
border-right: 2px dashed $c;
|
||||
pointer-events: none;
|
||||
width: 1px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
background: gray;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
//background: $c;
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
transform: $transform;
|
||||
border-left: $wHalf solid transparent;
|
||||
border-right: $wHalf solid transparent;
|
||||
border-top: $wHalf solid $c;
|
||||
}
|
||||
|
||||
&:after {
|
||||
bottom: 0;
|
||||
transform: $transform rotate(180deg);
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .icon-arrow-down {
|
||||
font-size: large;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: -8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<template>
|
||||
<div class="l-browse-bar">
|
||||
<div class="l-browse-bar" aria-label="Browse bar">
|
||||
<div class="l-browse-bar__start">
|
||||
<button
|
||||
v-if="hasParent"
|
||||
@ -35,6 +35,7 @@
|
||||
</div>
|
||||
<span
|
||||
ref="objectName"
|
||||
aria-label="Browse bar object name"
|
||||
class="l-browse-bar__object-name c-object-label__name"
|
||||
:class="{ 'c-input-inline': isPersistable }"
|
||||
:contenteditable="isNameEditable"
|
||||
|
@ -80,13 +80,11 @@ class Browse {
|
||||
this.#openmct.layout.$refs.browseBar.viewKey = viewProvider.key;
|
||||
}
|
||||
|
||||
#updateDocumentTitleOnNameMutation(newName) {
|
||||
if (typeof newName === 'string' && newName !== document.title) {
|
||||
document.title = newName;
|
||||
this.#openmct.layout.$refs.browseBar.domainObject = {
|
||||
...this.#openmct.layout.$refs.browseBar.domainObject,
|
||||
name: newName
|
||||
};
|
||||
#handleBrowseObjectUpdate(newObject) {
|
||||
this.#openmct.layout.$refs.browseBar.domainObject = newObject;
|
||||
|
||||
if (typeof newObject.name === 'string' && newObject.name !== document.title) {
|
||||
document.title = newObject.name;
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,8 +118,8 @@ class Browse {
|
||||
document.title = this.#browseObject.name; //change document title to current object in main view
|
||||
this.#unobserve = this.#openmct.objects.observe(
|
||||
this.#browseObject,
|
||||
'name',
|
||||
this.#updateDocumentTitleOnNameMutation.bind(this)
|
||||
'*',
|
||||
this.#handleBrowseObjectUpdate.bind(this)
|
||||
);
|
||||
const currentProvider = this.#openmct.objectViews.getByProviderKey(currentViewKey);
|
||||
if (currentProvider && currentProvider.canView(this.#browseObject, this.#openmct.router.path)) {
|
||||
|
Reference in New Issue
Block a user