Compare commits

...

65 Commits

Author SHA1 Message Date
David Tsay
51453c04d0 fix logical reporting overriding previous logical validation 2025-03-31 13:53:16 -07:00
David Tsay
1d655e5ccf fix broken limit check 2025-03-31 13:24:22 -07:00
David Tsay
e25bfb7291 don't override time conductor defaults 2025-03-26 15:52:10 -07:00
David Tsay
38457fd262 add back date selector 2024-11-14 12:02:38 -08:00
David Tsay
9ad00a226c fix input validation and reporting 2024-11-07 16:19:03 -08:00
David Tsay
16a4ca7a9c cherry-pick # 7875 c43ef64733cc96c5522f89b217a4534fdc36a7b8 2024-10-21 16:07:42 -07:00
David Tsay
eab689dbce cherry-pick #7858 4415fe7952049023b5e4f0ec2fa3bde8ec1296b2 2024-10-21 16:04:12 -07:00
David Tsay
9cfbb966e2 cherry-pick #7863 83e4a124e2a6727651d82acd49eb1854b4f105ac 2024-10-21 15:50:23 -07:00
David Tsay
50e60e7824 cherry-pick #7810 fccae3bd49dda693fcd8a8e6bae964daf2fd4b6d 2024-10-21 15:47:39 -07:00
David Tsay
6e2f667b50 cherry-pick #7882 2b8673941a8e81c5ac6d4ffb580a17870ceac2c5 2024-10-21 15:16:46 -07:00
David Tsay
cef02d60b1 fix datetime for consistency 2024-10-21 14:49:07 -07:00
David Tsay
98f8bc494a fix input entry and validation 2024-10-21 10:42:05 -07:00
David Tsay
cbb5dcea39 cherry-pick #7850 2024-09-27 14:37:19 -07:00
David Tsay
e2a17b5f06 Merge tag 'v4.0.0' into omm-5.3-openmct-4.0.0 2024-09-26 14:49:55 -07:00
David Tsay
e86c2a1d11 handle toggle independent time 2024-08-22 09:31:10 -07:00
David Tsay
a0291879e9 move history out of form
remove unused gear icon
remove debugging code
2024-08-22 09:31:10 -07:00
Jesse Mazzella
e664afd468
chore: bump version to 4.0.0 (#7813) 2024-08-14 11:48:36 -07:00
Jesse Mazzella
d786452abe
cherry-pick(#7806): chore: re-enable perf/mem tests on PR + fix broken locator in imagery perf test (#7812)
chore: re-enable perf/mem tests on PR + fix broken locator in imagery perf test (#7806)

* test: fix broken locator in imagery perf test

* Prevent this from happening

* make rule explicit

* test: maintain `locator()` pattern for contract tests

* test(couchdb): try some new techniques to stabilize the test

* Revert "test(couchdb): try some new techniques to stabilize the test"

This reverts commit 9aa1ea95a1035238ec2c86bfd84eaab0178cca1b.

* chore: revert to `networkidle` and disable eslint rule

* test: add `@network` annotation for tests with real network requests

---------

Co-authored-by: Hill, John (ARC-TI)[KBR Wyle Services, LLC] <john.c.hill@nasa.gov>
2024-08-13 16:00:43 -07:00
David Tsay
5cce8d747c Merge branch 'time-conductor-options' into omm-release/5.3-next 2024-07-16 16:01:28 -07:00
David Tsay
3922ade19f time formats can specify date/time delimiters 2024-07-12 15:46:38 -07:00
David Tsay
e2092a7b17 fix css for bounds single inputs 2024-07-12 15:46:07 -07:00
David Tsay
9065158ac9 make date time only for supporting time systems 2024-07-10 18:26:30 -07:00
David Tsay
9924f53128 only change time options if independent conductor enabled
re-use timeContext across composables
2024-07-10 18:25:31 -07:00
David Tsay
eb314a6fab make message universal to all timeSystems 2024-07-10 18:22:01 -07:00
David Tsay
fbf145c240 rename because need both datetime and time inputs 2024-07-03 11:35:05 -07:00
David Tsay
b3c99aef80 update to use composables 2024-07-03 11:33:36 -07:00
David Tsay
f1cf12d412 change ITC to use composables 2024-07-03 11:33:18 -07:00
David Tsay
7ace3d00be remove unused injection 2024-07-03 11:32:59 -07:00
David Tsay
1d77368c7c edit for clarity 2024-07-03 11:29:10 -07:00
David Tsay
1aa30975e5 use reactive clock props
cleanup unused variables
2024-06-10 18:24:17 -07:00
David Tsay
f3493907a2 use reactive timecontext in fixed inputs 2024-06-06 16:57:40 -07:00
David Tsay
0cbdbc6807 switch to reactive formatter for zoom 2024-06-06 16:17:12 -07:00
David Tsay
0254367cd5 fix missing clock on independent time mode change 2024-06-06 14:50:21 -07:00
David Tsay
a0299820ed better jsdoc message 2024-06-06 14:49:44 -07:00
David Tsay
0168f92a23 incorporate provided timecontext into independant time conductor components 2024-06-06 13:13:49 -07:00
David Tsay
9e56a22bd9 provide timecontext even if its the global/timeapi for shared components 2024-06-05 16:29:48 -07:00
David Tsay
e479c913e5 WIP: mostly fixed independent time conductor 2024-05-22 12:09:23 -07:00
David Tsay
9a577923d9 vue component naming conventions 2024-05-21 17:34:42 -07:00
David Tsay
1bb49ead0a WIP: get independent time contexts working again 2024-05-21 17:34:18 -07:00
David Tsay
5dba73e83c use shallowRef for clock object 2024-05-21 11:26:41 -07:00
David Tsay
0776003f40 reactivity and non fixes 2024-05-17 17:12:38 -07:00
David Tsay
11adbd283a change timeContexts to be reactive to objectPath
reactivity fixes to composables
2024-05-17 16:13:50 -07:00
David Tsay
852ee74094 change composables to allow for timeContexts 2024-05-16 12:37:47 -07:00
David Tsay
bd1c7d9da0 vue naming convention 2024-05-14 15:35:58 -07:00
David Tsay
23e12fa0d1 whoops. mistook prettier red squiggly for code not used red squiggly. 2024-05-14 15:35:37 -07:00
David Tsay
3788a132c4 create useClock composable
code clean up
2024-05-13 18:06:58 -07:00
David Tsay
4f37d955eb use composables for conductor mode 2024-05-01 12:06:08 -07:00
David Tsay
2c559457f0 missed file in commit 2024-04-19 15:28:32 -07:00
David Tsay
00c9d2920b WIP change replication in time api calls to use composables
many things broken
will need refactor to account for independent time conductor
2024-04-19 15:28:17 -07:00
David Tsay
e624821ab1 add useClockOffsets composable
documentation update to useTimeBounds
2024-04-17 17:15:40 -07:00
David Tsay
15126fd550 add useTimeBounds composable 2024-04-17 16:12:36 -07:00
David Tsay
5adc86c71e add useTimeMode composable
fix listener in useTimeSystem
2024-04-17 14:16:25 -07:00
David Tsay
a11fcc3231 return reactive isUTCBased 2024-04-17 11:32:10 -07:00
David Tsay
d0a77637c0 extend useTimeSystem to return reactive formatters 2024-04-17 11:22:31 -07:00
David Tsay
eeda4c62fc forgot to push useTimeSystem 2024-04-17 10:46:34 -07:00
David Tsay
10457a583e Merge branch 'master' into time-conductor-options 2024-04-17 10:44:56 -07:00
David Tsay
ff605a0a0d create useTimeSystem composable 2024-04-16 16:44:32 -07:00
David Tsay
f56a8a1f5d Revert "script, template, style"
This reverts commit c8f8a098f2649cc8d4518edd4dc657f93d02d727.
2024-04-15 19:43:22 -07:00
David Tsay
c8f8a098f2 script, template, style 2024-04-15 18:22:58 -07:00
David Tsay
31be2cba3d use vue component naming convention 2024-04-15 16:04:48 -07:00
David Tsay
eeb4f995a4 add missing licensing comment 2024-04-15 15:52:35 -07:00
Jesse Mazzella
40373abfe3
fix: 2d canvas fallback logic (#7295) 2023-12-12 15:13:01 -08:00
Scott Bell
86d4244ace
cherry-pick(#7262): Update API documentation for Visibility-Based Rendering (#7267)
Update API documentation for Visibility-Based Rendering (#7262)

update API with documentation for Visibility-Based Rendering
2023-12-01 10:33:09 -05:00
Jesse Mazzella
da299e9b95
chore: bump version to 3.2.0 (#7266) 2023-11-30 14:18:54 -08:00
Scott Bell
f0dcf2ba21
cherry-pick((#7241) Provide visibility based rendering as part of the view api (#7249)
Provide visibility based rendering as part of the view api (#7241)

* first draft

* in preview mode, just show it

* fix unit tests
2023-11-20 18:50:31 +01:00
70 changed files with 3522 additions and 1989 deletions

View File

@ -8,8 +8,8 @@ executors:
- image: mcr.microsoft.com/playwright:v1.45.2-focal - image: mcr.microsoft.com/playwright:v1.45.2-focal
environment: environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps PERCY_POSTINSTALL_BROWSER: "true" # Needed to store the percy browser in cache deps
PERCY_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742) PERCY_LOGLEVEL: "debug" # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
PERCY_PARALLEL_TOTAL: 2 PERCY_PARALLEL_TOTAL: 2
ubuntu: ubuntu:
machine: machine:
@ -17,7 +17,7 @@ executors:
docker_layer_caching: true docker_layer_caching: true
commands: commands:
build_and_install: build_and_install:
description: 'All steps used to build and install.' description: "All steps used to build and install."
parameters: parameters:
node-version: node-version:
type: string type: string
@ -27,7 +27,7 @@ commands:
node-version: << parameters.node-version >> node-version: << parameters.node-version >>
- node/install-packages - node/install-packages
generate_and_store_version_and_filesystem_artifacts: generate_and_store_version_and_filesystem_artifacts:
description: 'Track important packages and files' description: "Track important packages and files"
steps: steps:
- run: | - run: |
[[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts) [[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts)
@ -38,7 +38,7 @@ commands:
- store_artifacts: - store_artifacts:
path: /tmp/artifacts/ path: /tmp/artifacts/
generate_e2e_code_cov_report: generate_e2e_code_cov_report:
description: 'Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test' description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test"
parameters: parameters:
suite: suite:
type: string type: string
@ -102,7 +102,7 @@ jobs:
node-version: lts/hydrogen node-version: lts/hydrogen
- when: #Only install chrome-beta when running the 'full' suite to save $$$ - when: #Only install chrome-beta when running the 'full' suite to save $$$
condition: condition:
equal: ['full', <<parameters.suite>>] equal: ["full", <<parameters.suite>>]
steps: steps:
- run: npx playwright install chrome-beta - run: npx playwright install chrome-beta
- run: - run:
@ -259,6 +259,8 @@ workflows:
- visual-a11y: - visual-a11y:
name: visual-a11y-ci name: visual-a11y-ci
suite: ci suite: ci
- perf-test
- mem-test
the-nightly: #These jobs do not run on PRs, but against master at night the-nightly: #These jobs do not run on PRs, but against master at night
jobs: jobs:
@ -282,7 +284,7 @@ workflows:
- e2e-couchdb - e2e-couchdb
triggers: triggers:
- schedule: - schedule:
cron: '0 0 * * *' cron: "0 0 * * *"
filters: filters:
branches: branches:
only: only:

View File

@ -20,6 +20,13 @@ module.exports = {
'playwright/no-get-by-title': 'error', 'playwright/no-get-by-title': 'error',
'playwright/prefer-comparison-matcher': 'error' 'playwright/prefer-comparison-matcher': 'error'
} }
},
{
// Disable no-raw-locators for .contract.perf.spec.js files until https://github.com/grafana/xk6-browser/issues/1226
files: ['**/*.contract.perf.spec.js'],
rules: {
'playwright/no-raw-locators': 'off'
}
} }
] ]
}; };

View File

@ -130,7 +130,7 @@ test.describe('Persistence operations @addInit', () => {
}); });
}); });
test.describe('Persistence operations @couchdb', () => { test.describe('Persistence operations @couchdb @network', () => {
test.use({ failOnConsoleError: false }); test.use({ failOnConsoleError: false });
test('Editing object properties should generate a single persistence operation', async ({ test('Editing object properties should generate a single persistence operation', async ({
page page
@ -170,7 +170,7 @@ test.describe('Persistence operations @couchdb', () => {
}) })
.toEqual(1); .toEqual(1);
}); });
test('Can create an object after a conflict error @couchdb @2p', async ({ test('Can create an object after a conflict error @couchdb @network @2p', async ({
page, page,
openmctConfig openmctConfig
}) => { }) => {

View File

@ -429,7 +429,7 @@ test.describe('Display Layout', () => {
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden(); await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
}); });
test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb', async ({ test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb @network', async ({
page page
}) => { }) => {
await setFixedTimeMode(page); await setFixedTimeMode(page);

View File

@ -24,20 +24,26 @@
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks with CouchDB. This test suite is dedicated to tests which verify the basic operations surrounding Notebooks with CouchDB.
*/ */
/**
* Disable no-networkidle eslint rule until we can engineer more deterministic network-event
* driven tests.
*/
/* eslint-disable playwright/no-networkidle */
import { createDomainObjectWithDefaults } from '../../../../appActions.js'; import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import * as nbUtils from '../../../../helper/notebookUtils.js'; import * as nbUtils from '../../../../helper/notebookUtils.js';
import { expect, test } from '../../../../pluginFixtures.js'; import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Notebook Tests with CouchDB @couchdb', () => { test.describe('Notebook Tests with CouchDB @couchdb @network', () => {
let testNotebook; let testNotebook;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// Navigate to baseURL // Navigate to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'networkidle' });
// Create Notebook // Create Notebook
testNotebook = await createDomainObjectWithDefaults(page, { type: 'Notebook' }); testNotebook = await createDomainObjectWithDefaults(page, { type: 'Notebook' });
await page.goto(testNotebook.url, { waitUntil: 'domcontentloaded' }); await page.goto(testNotebook.url, { waitUntil: 'networkidle' });
}); });
test('Inspect Notebook Entry Network Requests', async ({ page }) => { test('Inspect Notebook Entry Network Requests', async ({ page }) => {
@ -58,7 +64,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
page.getByLabel('Add Page').click() page.getByLabel('Add Page').click()
]); ]);
// Ensures that there are no other network requests // Ensures that there are no other network requests
await page.waitForLoadState('domcontentloaded'); await page.waitForLoadState('networkidle');
// Assert that only two requests are made // Assert that only two requests are made
// Network Requests are: // Network Requests are:
@ -77,7 +83,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
// 2) The shared worker event from 👆 POST request // 2) The shared worker event from 👆 POST request
notebookElementsRequests = []; notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'First Entry'); await nbUtils.enterTextEntry(page, 'First Entry');
await page.waitForLoadState('domcontentloaded'); await page.waitForLoadState('networkidle');
expect(notebookElementsRequests.length).toBeLessThanOrEqual(2); expect(notebookElementsRequests.length).toBeLessThanOrEqual(2);
// Add some tags // Add some tags
@ -141,7 +147,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
// 4) The shared worker event from 👆 POST request // 4) The shared worker event from 👆 POST request
notebookElementsRequests = []; notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'Fourth Entry'); await nbUtils.enterTextEntry(page, 'Fourth Entry');
page.waitForLoadState('domcontentloaded'); page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
@ -153,7 +159,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
// 4) The shared worker event from 👆 POST request // 4) The shared worker event from 👆 POST request
notebookElementsRequests = []; notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'Fifth Entry'); await nbUtils.enterTextEntry(page, 'Fifth Entry');
page.waitForLoadState('domcontentloaded'); page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
@ -164,7 +170,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
// 4) The shared worker event from 👆 POST request // 4) The shared worker event from 👆 POST request
notebookElementsRequests = []; notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'Sixth Entry'); await nbUtils.enterTextEntry(page, 'Sixth Entry');
page.waitForLoadState('domcontentloaded'); page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
}); });
@ -227,7 +233,7 @@ async function addTagAndAwaitNetwork(page, tagName) {
page.locator(`[aria-label="Autocomplete Options"] >> text=${tagName}`).click(), page.locator(`[aria-label="Autocomplete Options"] >> text=${tagName}`).click(),
expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeVisible() expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeVisible()
]); ]);
await page.waitForLoadState('domcontentloaded'); await page.waitForLoadState('networkidle');
} }
/** /**
@ -246,5 +252,5 @@ async function removeTagAndAwaitNetwork(page, tagName) {
) )
]); ]);
await expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeHidden(); await expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeHidden();
await page.waitForLoadState('domcontentloaded'); await page.waitForLoadState('networkidle');
} }

View File

@ -57,7 +57,7 @@ test.describe('Tabs View', () => {
await page.goto(tabsView.url); await page.goto(tabsView.url);
// select first tab // select first tab
await page.getByLabel(`${table.name} tab`, { exact: true }).click(); await page.getByLabel(`${table.name} tab - selected`, { exact: true }).click();
// ensure table header visible // ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible(); await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
@ -92,6 +92,38 @@ test.describe('Tabs View', () => {
// no canvas (i.e., sine wave generator) in the document should be visible // no canvas (i.e., sine wave generator) in the document should be visible
await expect(page.locator('canvas[id=webglContext]')).toBeHidden(); await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
}); });
test('Changing the displayed tab should not be persisted if the view is locked', async ({
page
}) => {
await page.goto(tabsView.url);
//lock the view
await page.getByLabel('Unlocked for editing, click to lock.', { exact: true }).click();
// get the initial tab index
const initialTab = page.getByLabel(/- selected/);
// switch to a different tab in the view
const swgTab = page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true });
await swgTab.click();
await page.getByLabel(`${sineWaveGenerator.name} Object View`).isVisible();
// navigate away from the tabbed view and back
await page.getByRole('treeitem', { name: 'My Items' }).click();
await page.goto(tabsView.url);
// check that the initial tab is displayed
const lockedSelectedTab = page.getByLabel(/- selected/);
await expect(lockedSelectedTab).toHaveText(await initialTab.textContent());
//unlock the view
await page.getByLabel('Locked for editing. Click to unlock.', { exact: true }).click();
// switch to a different tab in the view
await swgTab.click();
await page.getByLabel(`${sineWaveGenerator.name} Object View`).isVisible();
// navigate away from the tabbed view and back
await page.getByRole('treeitem', { name: 'My Items' }).click();
await page.goto(tabsView.url);
// check that the newly selected tab is displayed
const unlockedSelectedTab = page.getByLabel(/- selected/);
await expect(unlockedSelectedTab).toBeVisible();
});
}); });
test.describe('Tabs View CRUD', () => { test.describe('Tabs View CRUD', () => {

View File

@ -168,7 +168,7 @@ test.describe('Grand Search', () => {
await expect(page.getByText('No results found')).toBeVisible(); await expect(page.getByText('No results found')).toBeVisible();
}); });
test('Validate single object in search result @couchdb', async ({ page }) => { test('Validate single object in search result @couchdb @network', async ({ page }) => {
// Create a folder object // Create a folder object
const folderName = uuid(); const folderName = uuid();
await createDomainObjectWithDefaults(page, { await createDomainObjectWithDefaults(page, {
@ -191,7 +191,7 @@ test.describe('Grand Search', () => {
await expect(searchResults).toContainText(folderName); await expect(searchResults).toContainText(folderName);
}); });
test('Search results are debounced @couchdb', async ({ page }) => { test('Search results are debounced @couchdb @network', async ({ page }) => {
test.info().annotations.push({ test.info().annotations.push({
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6179' description: 'https://github.com/nasa/openmct/issues/6179'
@ -221,7 +221,9 @@ test.describe('Grand Search', () => {
await expect(page.getByRole('list', { name: 'Object Results' })).toContainText('Clock A'); await expect(page.getByRole('list', { name: 'Object Results' })).toContainText('Clock A');
}); });
test('Slowly typing after search debounce will abort requests @couchdb', async ({ page }) => { test('Slowly typing after search debounce will abort requests @couchdb @network', async ({
page
}) => {
let requestWasAborted = false; let requestWasAborted = false;
await createObjectsForSearch(page); await createObjectsForSearch(page);
page.on('requestfailed', (request) => { page.on('requestfailed', (request) => {

View File

@ -28,7 +28,7 @@ test.describe('Main Tree', () => {
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
}); });
test('Creating a child object within a folder and immediately opening it shows the created object in the tree @couchdb', async ({ test('Creating a child object within a folder and immediately opening it shows the created object in the tree @couchdb @network', async ({
page page
}) => { }) => {
test.info().annotations.push({ test.info().annotations.push({
@ -88,7 +88,7 @@ test.describe('Main Tree', () => {
).toBeVisible(); ).toBeVisible();
}); });
test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @couchdb @2p', async ({ test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @couchdb @network @2p', async ({
page page
}) => { }) => {
test.info().annotations.push({ test.info().annotations.push({
@ -187,7 +187,7 @@ test.describe('Main Tree', () => {
]); ]);
}); });
}); });
test('Opening and closing an item before the request has been fulfilled will abort the request @couchdb', async ({ test('Opening and closing an item before the request has been fulfilled will abort the request @couchdb @network', async ({
page page
}) => { }) => {
let requestWasAborted = false; let requestWasAborted = false;

View File

@ -164,7 +164,7 @@ test.describe('Performance tests', () => {
await page.evaluate(() => window.performance.mark('background-image-visible')); await page.evaluate(() => window.performance.mark('background-image-visible'));
// Get Current number of images in thumbstrip // Get Current number of images in thumbstrip
await page.locator('.c-imagery__thumb').waitFor({ state: 'visible' }); await expect(page.locator('.c-imagery__thumb').last()).toBeInViewport();
const thumbCount = await page.locator('.c-imagery__thumb').count(); const thumbCount = await page.locator('.c-imagery__thumb').count();
console.log('number of thumbs rendered ' + thumbCount); console.log('number of thumbs rendered ' + thumbCount);
await page.locator('.c-imagery__thumb').last().click(); await page.locator('.c-imagery__thumb').last().click();

View File

@ -64,7 +64,7 @@ test.describe('Tabs View', () => {
page.goto(tabsView.url); page.goto(tabsView.url);
// select first tab // select first tab
await page.getByLabel(`${table.name} tab`, { exact: true }).click(); await page.getByLabel(`${table.name} tab - selected`, { exact: true }).click();
// ensure table header visible // ensure table header visible
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible(); await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();

View File

@ -1,6 +1,6 @@
{ {
"name": "openmct", "name": "openmct",
"version": "4.0.0-next", "version": "4.0.0",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"module": "dist/openmct.js", "module": "dist/openmct.js",
"main": "dist/openmct.js", "main": "dist/openmct.js",

View File

@ -231,26 +231,20 @@ export default class TelemetryAPI {
* @returns {TelemetryRequestOptions} the options, with defaults filled in * @returns {TelemetryRequestOptions} the options, with defaults filled in
*/ */
standardizeRequestOptions(options = {}) { standardizeRequestOptions(options = {}) {
if (!Object.hasOwn(options, 'start')) { if (!Object.hasOwn(options, 'timeContext')) {
const bounds = options.timeContext?.getBounds(); options.timeContext = this.openmct.time;
if (bounds?.start) {
options.start = options.timeContext.getBounds().start;
} else {
options.start = this.openmct.time.getBounds().start;
}
}
if (!Object.hasOwn(options, 'end')) {
const bounds = options.timeContext?.getBounds();
if (bounds?.end) {
options.end = options.timeContext.getBounds().end;
} else {
options.end = this.openmct.time.getBounds().end;
}
} }
if (!Object.hasOwn(options, 'domain')) { if (!Object.hasOwn(options, 'domain')) {
options.domain = this.openmct.time.getTimeSystem().key; options.domain = options.timeContext.getTimeSystem().key;
}
if (!Object.hasOwn(options, 'start')) {
options.start = options.timeContext.getBounds().start;
}
if (!Object.hasOwn(options, 'end')) {
options.end = options.timeContext.getBounds().end;
} }
return options; return options;

View File

@ -269,36 +269,40 @@ describe('Telemetry API', () => {
await telemetryAPI.request(domainObject); await telemetryAPI.request(domainObject);
const { signal } = new AbortController(); const { signal } = new AbortController();
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), { expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, {
signal, signal,
start: 0, start: 0,
end: 1, end: 1,
domain: 'system' domain: 'system',
timeContext: openmct.time
}); });
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), { expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, {
signal, signal,
start: 0, start: 0,
end: 1, end: 1,
domain: 'system' domain: 'system',
timeContext: openmct.time
}); });
telemetryProvider.supportsRequest.calls.reset(); telemetryProvider.supportsRequest.calls.reset();
telemetryProvider.request.calls.reset(); telemetryProvider.request.calls.reset();
await telemetryAPI.request(domainObject, {}); await telemetryAPI.request(domainObject, {});
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), { expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, {
signal, signal,
start: 0, start: 0,
end: 1, end: 1,
domain: 'system' domain: 'system',
timeContext: openmct.time
}); });
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), { expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, {
signal, signal,
start: 0, start: 0,
end: 1, end: 1,
domain: 'system' domain: 'system',
timeContext: openmct.time
}); });
}); });
@ -313,18 +317,20 @@ describe('Telemetry API', () => {
domain: 'someDomain' domain: 'someDomain'
}); });
const { signal } = new AbortController(); const { signal } = new AbortController();
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(jasmine.any(Object), { expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(domainObject, {
start: 20, start: 20,
end: 30, end: 30,
domain: 'someDomain', domain: 'someDomain',
signal signal,
timeContext: openmct.time
}); });
expect(telemetryProvider.request).toHaveBeenCalledWith(jasmine.any(Object), { expect(telemetryProvider.request).toHaveBeenCalledWith(domainObject, {
start: 20, start: 20,
end: 30, end: 30,
domain: 'someDomain', domain: 'someDomain',
signal signal,
timeContext: openmct.time
}); });
}); });
describe('telemetry batching support', () => { describe('telemetry batching support', () => {

View File

@ -62,9 +62,6 @@ export default class TelemetryCollection extends EventEmitter {
this.futureBuffer = []; this.futureBuffer = [];
this.parseTime = undefined; this.parseTime = undefined;
this.metadata = this.openmct.telemetry.getMetadata(domainObject); this.metadata = this.openmct.telemetry.getMetadata(domainObject);
if (!Object.hasOwn(options, 'timeContext')) {
options.timeContext = this.openmct.time;
}
this.options = options; this.options = options;
this.unsubscribe = undefined; this.unsubscribe = undefined;
this.pageState = undefined; this.pageState = undefined;
@ -84,6 +81,9 @@ export default class TelemetryCollection extends EventEmitter {
this._error(LOADED_ERROR); this._error(LOADED_ERROR);
} }
if (!Object.hasOwn(this.options, 'timeContext')) {
this.options.timeContext = this.openmct.time;
}
this._setTimeSystem(this.options.timeContext.getTimeSystem()); this._setTimeSystem(this.options.timeContext.getTimeSystem());
this.lastBounds = this.options.timeContext.getBounds(); this.lastBounds = this.options.timeContext.getBounds();
this._watchBounds(); this._watchBounds();
@ -128,7 +128,7 @@ export default class TelemetryCollection extends EventEmitter {
* @private * @private
*/ */
async _requestHistoricalTelemetry() { async _requestHistoricalTelemetry() {
let options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options }); const options = this.openmct.telemetry.standardizeRequestOptions({ ...this.options });
const historicalProvider = this.openmct.telemetry.findRequestProvider( const historicalProvider = this.openmct.telemetry.findRequestProvider(
this.domainObject, this.domainObject,
options options

View File

@ -321,9 +321,17 @@ class IndependentTimeContext extends TimeContext {
return this.upstreamTimeContext.setMode(...arguments); return this.upstreamTimeContext.setMode(...arguments);
} }
if (mode === MODES.realtime && this.activeClock === undefined) { if (mode === MODES.realtime) {
// TODO: This should probably happen up front in creating an independent time context
// TODO: not just in time every time setMode is called
if (this.activeClock === undefined) {
this.activeClock = this.globalTimeContext.getClock();
}
if (this.activeClock === undefined) {
throw `Unknown clock. Has a clock been registered with 'addClock'?`; throw `Unknown clock. Has a clock been registered with 'addClock'?`;
} }
}
if (mode !== this.mode) { if (mode !== this.mode) {
this.mode = mode; this.mode = mode;

View File

@ -134,30 +134,30 @@ class TimeAPI extends GlobalTimeContext {
/** /**
* Get or set an independent time context which follows the TimeAPI timeSystem, * Get or set an independent time context which follows the TimeAPI timeSystem,
* but with different offsets for a given domain object * but with different bounds for a given domain object
* @param {string} key The identifier key of the domain object these offsets are set for * @param {string} keyString The keyString identifier of the domain object these offsets are set for
* @param {ClockOffsets | TimeConductorBounds} value This maintains a sliding time window of a fixed width that automatically updates * @param {TimeConductorBounds | ClockOffsets} boundsOrOffsets either bounds if in fixed mode, or offsets if in realtime mode
* @param {key | string} clockKey the real time clock key currently in use * @param {string} clockKey the key for the real time clock to use
*/ */
addIndependentContext(key, value, clockKey) { addIndependentContext(keyString, boundsOrOffsets, clockKey) {
let timeContext = this.getIndependentContext(key); let timeContext = this.getIndependentContext(keyString);
//stop following upstream time context since the view has it's own //stop following upstream time context since the view has it's own
timeContext.resetContext(); timeContext.resetContext();
if (clockKey) { if (clockKey) {
timeContext.setClock(clockKey); timeContext.setClock(clockKey);
timeContext.setMode(REALTIME_MODE_KEY, value); timeContext.setMode(REALTIME_MODE_KEY, boundsOrOffsets);
} else { } else {
timeContext.setMode(FIXED_MODE_KEY, value); timeContext.setMode(FIXED_MODE_KEY, boundsOrOffsets);
} }
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context // Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
this.emit('refreshContext', key); this.emit('refreshContext', keyString);
return () => { return () => {
//follow any upstream time context //follow any upstream time context
this.emit('removeOwnContext', key); this.emit('removeOwnContext', keyString);
}; };
} }

View File

@ -196,7 +196,7 @@ class TimeContext extends EventEmitter {
} else if (bounds.start > bounds.end) { } else if (bounds.start > bounds.end) {
return { return {
valid: false, valid: false,
message: 'Specified start date exceeds end bound' message: 'Start bound exceeds end bound'
}; };
} }

View File

@ -69,7 +69,6 @@ const INNER_TEXT_PADDING = 15;
const TEXT_LEFT_PADDING = 5; const TEXT_LEFT_PADDING = 5;
const ROW_PADDING = 5; const ROW_PADDING = 5;
const SWIMLANE_PADDING = 3; const SWIMLANE_PADDING = 3;
const RESIZE_POLL_INTERVAL = 200;
const ROW_HEIGHT = 22; const ROW_HEIGHT = 22;
const MAX_TEXT_WIDTH = 300; const MAX_TEXT_WIDTH = 300;
const MIN_ACTIVITY_WIDTH = 2; const MIN_ACTIVITY_WIDTH = 2;
@ -143,13 +142,15 @@ export default {
this.canvasContext = canvas.getContext('2d'); this.canvasContext = canvas.getContext('2d');
this.setDimensions(); this.setDimensions();
this.setTimeContext(); this.setTimeContext();
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
this.handleConfigurationChange(this.configuration); this.handleConfigurationChange(this.configuration);
this.planViewConfiguration.on('change', this.handleConfigurationChange); this.planViewConfiguration.on('change', this.handleConfigurationChange);
this.loadComposition(); this.loadComposition();
this.resizeObserver = new ResizeObserver(this.resize);
this.resizeObserver.observe(this.$refs.plan);
}, },
beforeUnmount() { beforeUnmount() {
clearInterval(this.resizeTimer); this.resizeObserver.disconnect();
this.stopFollowingTimeContext(); this.stopFollowingTimeContext();
if (this.unlisten) { if (this.unlisten) {
this.unlisten(); this.unlisten();

View File

@ -575,6 +575,7 @@ export default {
this.buildCanvasElements(); this.buildCanvasElements();
this.drawAPI = DrawLoader.getFallbackDrawAPI(this.canvas, this.overlay); this.drawAPI = DrawLoader.getFallbackDrawAPI(this.canvas, this.overlay);
this.$emit('plot-reinitialize-canvas'); this.$emit('plot-reinitialize-canvas');
console.warn(`📈 fallback to 2D canvas`);
}, },
removeChartElement(series) { removeChartElement(series) {
const elements = this.seriesElements.get(toRaw(series)); const elements = this.seriesElements.get(toRaw(series));

View File

@ -152,17 +152,20 @@ export default class RemoteClock extends DefaultClock {
*/ */
#waitForReady() { #waitForReady() {
const waitForInitialTick = (resolve) => { const waitForInitialTick = (resolve) => {
const tickListener = () => {
if (this.lastTick > 0) { if (this.lastTick > 0) {
const offsets = this.openmct.time.getClockOffsets(); const offsets = this.openmct.time.getClockOffsets();
this.openmct.time.off('tick', tickListener); // Unregister the tick listener
resolve({ resolve({
start: this.lastTick + offsets.start, start: this.lastTick + offsets.start,
end: this.lastTick + offsets.end end: this.lastTick + offsets.end
}); });
} else {
setTimeout(() => waitForInitialTick(resolve), 100);
} }
}; };
this.openmct.time.on('tick', tickListener);
};
return new Promise(waitForInitialTick); return new Promise(waitForInitialTick);
} }
} }

View File

@ -20,16 +20,43 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/**
* Intercepts requests to ensure the remote clock is ready.
*
* @param {import('../../openmct').OpenMCT} openmct - The OpenMCT instance.
* @param {import('../../openmct').Identifier} _remoteClockIdentifier - The identifier for the remote clock.
* @param {Function} waitForBounds - A function that returns a promise resolving to the initial bounds.
* @returns {Object} The request interceptor.
*/
function remoteClockRequestInterceptor(openmct, _remoteClockIdentifier, waitForBounds) { function remoteClockRequestInterceptor(openmct, _remoteClockIdentifier, waitForBounds) {
let remoteClockLoaded = false; let remoteClockLoaded = false;
return { return {
appliesTo: () => { /**
* Determines if the interceptor applies to the given request.
*
* @param {Object} _ - Unused parameter.
* @param {import('../../api/telemetry/TelemetryAPI').TelemetryRequestOptions} request - The request object.
* @returns {boolean} True if the interceptor applies, false otherwise.
*/
appliesTo: (_, request) => {
// Get the activeClock from the Global Time Context // Get the activeClock from the Global Time Context
/** @type {import("../../api/time/TimeContext").default} */
const { activeClock } = openmct.time; const { activeClock } = openmct.time;
// this type of request does not rely on clock having bounds
if (request.strategy === 'latest' && request.timeContext.isRealTime()) {
return false;
}
return activeClock?.key === 'remote-clock' && !remoteClockLoaded; return activeClock?.key === 'remote-clock' && !remoteClockLoaded;
}, },
/**
* Invokes the interceptor to modify the request.
*
* @param {Object} request - The request object.
* @returns {Promise<Object>} The modified request object.
*/
invoke: async (request) => { invoke: async (request) => {
const timeContext = request?.timeContext ?? openmct.time; const timeContext = request?.timeContext ?? openmct.time;

View File

@ -38,11 +38,11 @@
v-for="(tab, index) in tabsList" v-for="(tab, index) in tabsList"
:ref="tab.keyString" :ref="tab.keyString"
:key="tab.keyString" :key="tab.keyString"
:aria-label="`${tab.domainObject.name} tab`" :aria-label="`${tab.domainObject.name} tab${tab.keyString === currentTab.keyString ? ' - selected' : ''}`"
class="c-tab c-tabs-view__tab js-tab" class="c-tab c-tabs-view__tab js-tab"
role="tab" role="tab"
:class="{ :class="{
'is-current': isCurrent(tab) 'is-current': tab.keyString === currentTab.keyString
}" }"
@click="showTab(tab, index)" @click="showTab(tab, index)"
@mouseover.ctrl="showToolTip(tab)" @mouseover.ctrl="showToolTip(tab)"
@ -74,7 +74,7 @@
:key="tab.keyString" :key="tab.keyString"
:style="getTabStyles(tab)" :style="getTabStyles(tab)"
class="c-tabs-view__object-holder" class="c-tabs-view__object-holder"
:class="{ 'c-tabs-view__object-holder--hidden': !isCurrent(tab) }" :class="{ 'c-tabs-view__object-holder--hidden': tab.keyString !== currentTab.keyString }"
> >
<ObjectView <ObjectView
v-if="shouldLoadTab(tab)" v-if="shouldLoadTab(tab)"
@ -353,7 +353,10 @@ export default {
this.internalDomainObject = domainObject; this.internalDomainObject = domainObject;
}, },
persistCurrentTabIndex(index) { persistCurrentTabIndex(index) {
//only persist if the domain object is not locked. The object mutate API will deal with whether the object is persistable or not.
if (!this.internalDomainObject.locked) {
this.openmct.objects.mutate(this.internalDomainObject, 'currentTabIndex', index); this.openmct.objects.mutate(this.internalDomainObject, 'currentTabIndex', index);
}
}, },
storeCurrentTabIndexInURL(index) { storeCurrentTabIndexInURL(index) {
let currentTabIndexInURL = this.openmct.router.getSearchParam(this.searchTabKey); let currentTabIndexInURL = this.openmct.router.getSearchParam(this.searchTabKey);

View File

@ -25,6 +25,7 @@ import _ from 'lodash';
import StalenessUtils from '../../utils/staleness.js'; import StalenessUtils from '../../utils/staleness.js';
import TableRowCollection from './collections/TableRowCollection.js'; import TableRowCollection from './collections/TableRowCollection.js';
import { MODE } from './constants.js';
import TelemetryTableColumn from './TelemetryTableColumn.js'; import TelemetryTableColumn from './TelemetryTableColumn.js';
import TelemetryTableConfiguration from './TelemetryTableConfiguration.js'; import TelemetryTableConfiguration from './TelemetryTableConfiguration.js';
import TelemetryTableNameColumn from './TelemetryTableNameColumn.js'; import TelemetryTableNameColumn from './TelemetryTableNameColumn.js';
@ -119,7 +120,7 @@ export default class TelemetryTable extends EventEmitter {
this.rowLimit = rowLimit; this.rowLimit = rowLimit;
} }
if (this.telemetryMode === 'performance') { if (this.telemetryMode === MODE.PERFORMANCE) {
this.tableRows.setLimit(this.rowLimit); this.tableRows.setLimit(this.rowLimit);
} else { } else {
this.tableRows.removeLimit(); this.tableRows.removeLimit();
@ -129,14 +130,7 @@ export default class TelemetryTable extends EventEmitter {
createTableRowCollections() { createTableRowCollections() {
this.tableRows = new TableRowCollection(); this.tableRows = new TableRowCollection();
//Fetch any persisted default sort const sortOptions = this.configuration.getSortOptions();
let sortOptions = this.configuration.getConfiguration().sortOptions;
//If no persisted sort order, default to sorting by time system, descending.
sortOptions = sortOptions || {
key: this.openmct.time.getTimeSystem().key,
direction: 'desc'
};
this.updateRowLimit(); this.updateRowLimit();
@ -171,11 +165,10 @@ export default class TelemetryTable extends EventEmitter {
this.removeTelemetryCollection(keyString); this.removeTelemetryCollection(keyString);
let sortOptions = this.configuration.getConfiguration().sortOptions; let sortOptions = this.configuration.getSortOptions();
requestOptions.order = requestOptions.order = sortOptions.direction;
sortOptions?.direction ?? (this.telemetryMode === 'performance' ? 'desc' : 'asc');
if (this.telemetryMode === 'performance') { if (this.telemetryMode === MODE.PERFORMANCE) {
requestOptions.size = this.rowLimit; requestOptions.size = this.rowLimit;
requestOptions.enforceSize = true; requestOptions.enforceSize = true;
} }
@ -442,12 +435,13 @@ export default class TelemetryTable extends EventEmitter {
} }
sortBy(sortOptions) { sortBy(sortOptions) {
this.tableRows.sortBy(sortOptions); this.configuration.setSortOptions(sortOptions);
if (this.openmct.editor.isEditing()) { if (this.telemetryMode === MODE.PERFORMANCE) {
let configuration = this.configuration.getConfiguration(); this.tableRows.setSortOptions(sortOptions);
configuration.sortOptions = sortOptions; this.clearAndResubscribe();
this.configuration.updateConfiguration(configuration); } else {
this.tableRows.sortBy(sortOptions);
} }
} }

View File

@ -23,7 +23,11 @@
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import _ from 'lodash'; import _ from 'lodash';
import { ORDER } from './constants';
export default class TelemetryTableConfiguration extends EventEmitter { export default class TelemetryTableConfiguration extends EventEmitter {
#sortOptions;
constructor(domainObject, openmct, options) { constructor(domainObject, openmct, options) {
super(); super();
@ -44,6 +48,26 @@ export default class TelemetryTableConfiguration extends EventEmitter {
this.notPersistable = !this.openmct.objects.isPersistable(this.domainObject.identifier); this.notPersistable = !this.openmct.objects.isPersistable(this.domainObject.identifier);
} }
getSortOptions() {
return (
this.#sortOptions ||
this.getConfiguration().sortOptions || {
key: this.openmct.time.getTimeSystem().key,
direction: ORDER.DESCENDING
}
);
}
setSortOptions(sortOptions) {
this.#sortOptions = sortOptions;
if (this.openmct.editor.isEditing()) {
let configuration = this.getConfiguration();
configuration.sortOptions = sortOptions;
this.updateConfiguration(configuration);
}
}
getConfiguration() { getConfiguration() {
let configuration = this.domainObject.configuration || {}; let configuration = this.domainObject.configuration || {};
configuration.hiddenColumns = configuration.hiddenColumns || {}; configuration.hiddenColumns = configuration.hiddenColumns || {};

View File

@ -20,6 +20,8 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { MODE } from './constants.js';
export default function getTelemetryTableType(options) { export default function getTelemetryTableType(options) {
let { telemetryMode, persistModeChange, rowLimit } = options; let { telemetryMode, persistModeChange, rowLimit } = options;
@ -36,11 +38,11 @@ export default function getTelemetryTableType(options) {
control: 'select', control: 'select',
options: [ options: [
{ {
value: 'performance', value: MODE.PERFORMANCE,
name: 'Limited (Performance) Mode' name: 'Limited (Performance) Mode'
}, },
{ {
value: 'unlimited', value: MODE.UNLIMITED,
name: 'Unlimited Mode' name: 'Unlimited Mode'
} }
], ],

View File

@ -22,6 +22,7 @@
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import _ from 'lodash'; import _ from 'lodash';
import { ORDER } from '../constants.js';
/** /**
* @constructor * @constructor
*/ */
@ -149,13 +150,13 @@ export default class TableRowCollection extends EventEmitter {
} }
insertOrUpdateRows(rowsToAdd, addToBeginning) { insertOrUpdateRows(rowsToAdd, addToBeginning) {
rowsToAdd.forEach((row) => { rowsToAdd.forEach((row, addRowsIndex) => {
const index = this.getInPlaceUpdateIndex(row); const index = this.getInPlaceUpdateIndex(row);
if (index > -1) { if (index > -1) {
this.updateRowInPlace(row, index); this.updateRowInPlace(row, index);
} else { } else {
if (addToBeginning) { if (addToBeginning) {
this.rows.unshift(row); this.rows.splice(addRowsIndex, 0, row);
} else { } else {
this.rows.push(row); this.rows.push(row);
} }
@ -208,7 +209,7 @@ export default class TableRowCollection extends EventEmitter {
const val1 = this.getValueForSortColumn(row1); const val1 = this.getValueForSortColumn(row1);
const val2 = this.getValueForSortColumn(row2); const val2 = this.getValueForSortColumn(row2);
if (this.sortOptions.direction === 'asc') { if (this.sortOptions.direction === ORDER.ASCENDING) {
return val1 <= val2 ? row1 : row2; return val1 <= val2 ? row1 : row2;
} else { } else {
return val1 >= val2 ? row1 : row2; return val1 >= val2 ? row1 : row2;
@ -272,7 +273,7 @@ export default class TableRowCollection extends EventEmitter {
*/ */
sortBy(sortOptions) { sortBy(sortOptions) {
if (arguments.length > 0) { if (arguments.length > 0) {
this.sortOptions = sortOptions; this.setSortOptions(sortOptions);
this.rows = _.orderBy( this.rows = _.orderBy(
this.rows, this.rows,
(row) => row.getParsedValue(sortOptions.key), (row) => row.getParsedValue(sortOptions.key),
@ -285,6 +286,10 @@ export default class TableRowCollection extends EventEmitter {
return Object.assign({}, this.sortOptions); return Object.assign({}, this.sortOptions);
} }
setSortOptions(sortOptions) {
this.sortOptions = sortOptions;
}
setColumnFilter(columnKey, filter) { setColumnFilter(columnKey, filter) {
filter = filter.trim().toLowerCase(); filter = filter.trim().toLowerCase();
let wasBlank = this.columnFilters[columnKey] === undefined; let wasBlank = this.columnFilters[columnKey] === undefined;
@ -373,7 +378,7 @@ export default class TableRowCollection extends EventEmitter {
getRows() { getRows() {
if (this.rowLimit && this.rows.length > this.rowLimit) { if (this.rowLimit && this.rows.length > this.rowLimit) {
if (this.sortOptions.direction === 'desc') { if (this.sortOptions.direction === ORDER.DESCENDING) {
return this.rows.slice(0, this.rowLimit); return this.rows.slice(0, this.rowLimit);
} else { } else {
return this.rows.slice(-this.rowLimit); return this.rows.slice(-this.rowLimit);

View File

@ -296,6 +296,7 @@ import ProgressBar from '../../../ui/components/ProgressBar.vue';
import Search from '../../../ui/components/SearchComponent.vue'; import Search from '../../../ui/components/SearchComponent.vue';
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue'; import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import { useResizeObserver } from '../../../ui/composables/resize.js'; import { useResizeObserver } from '../../../ui/composables/resize.js';
import { MODE, ORDER } from '../constants.js';
import SizingRow from './SizingRow.vue'; import SizingRow from './SizingRow.vue';
import TableColumnHeader from './TableColumnHeader.vue'; import TableColumnHeader from './TableColumnHeader.vue';
import TableFooterIndicator from './TableFooterIndicator.vue'; import TableFooterIndicator from './TableFooterIndicator.vue';
@ -713,7 +714,7 @@ export default {
sortBy(columnKey) { sortBy(columnKey) {
let timeSystemKey = this.openmct.time.getTimeSystem().key; let timeSystemKey = this.openmct.time.getTimeSystem().key;
if (this.telemetryMode === 'performance' && columnKey !== timeSystemKey) { if (this.telemetryMode === MODE.PERFORMANCE && columnKey !== timeSystemKey) {
this.confirmUnlimitedMode('Switch to Unlimited Telemetry and Sort', () => { this.confirmUnlimitedMode('Switch to Unlimited Telemetry and Sort', () => {
this.initiateSort(columnKey); this.initiateSort(columnKey);
}); });
@ -724,15 +725,15 @@ export default {
initiateSort(columnKey) { initiateSort(columnKey) {
// If sorting by the same column, flip the sort direction. // If sorting by the same column, flip the sort direction.
if (this.sortOptions.key === columnKey) { if (this.sortOptions.key === columnKey) {
if (this.sortOptions.direction === 'asc') { if (this.sortOptions.direction === ORDER.ASCENDING) {
this.sortOptions.direction = 'desc'; this.sortOptions.direction = ORDER.DESCENDING;
} else { } else {
this.sortOptions.direction = 'asc'; this.sortOptions.direction = ORDER.ASCENDING;
} }
} else { } else {
this.sortOptions = { this.sortOptions = {
key: columnKey, key: columnKey,
direction: 'desc' direction: ORDER.DESCENDING
}; };
} }
@ -751,7 +752,7 @@ export default {
} }
}, },
shouldAutoScroll() { shouldAutoScroll() {
if (this.sortOptions.direction === 'desc') { if (this.sortOptions.direction === ORDER.DESCENDING) {
return false; return false;
} }
@ -844,7 +845,7 @@ export default {
return justTheData; return justTheData;
}, },
exportAllDataAsCSV() { exportAllDataAsCSV() {
if (this.telemetryMode === 'performance') { if (this.telemetryMode === MODE.PERFORMANCE) {
this.confirmUnlimitedMode('Switch to Unlimited Telemetry and Export', () => { this.confirmUnlimitedMode('Switch to Unlimited Telemetry and Export', () => {
const data = this.getTableRowData(); const data = this.getTableRowData();
@ -1226,7 +1227,8 @@ export default {
}); });
}, },
updateTelemetryMode() { updateTelemetryMode() {
this.telemetryMode = this.telemetryMode === 'unlimited' ? 'performance' : 'unlimited'; this.telemetryMode =
this.telemetryMode === MODE.UNLIMITED ? MODE.PERFORMANCE : MODE.UNLIMITED;
if (this.persistModeChange) { if (this.persistModeChange) {
this.table.configuration.setTelemetryMode(this.telemetryMode); this.table.configuration.setTelemetryMode(this.telemetryMode);
@ -1236,7 +1238,7 @@ export default {
const timeSystemKey = this.openmct.time.getTimeSystem().key; const timeSystemKey = this.openmct.time.getTimeSystem().key;
if (this.telemetryMode === 'performance' && this.sortOptions.key !== timeSystemKey) { if (this.telemetryMode === MODE.PERFORMANCE && this.sortOptions.key !== timeSystemKey) {
this.openmct.notifications.info( this.openmct.notifications.info(
'Switched to Performance Mode: Table now sorted by time for optimized efficiency.' 'Switched to Performance Mode: Table now sorted by time for optimized efficiency.'
); );

View File

@ -62,6 +62,8 @@
<script> <script>
import _ from 'lodash'; import _ from 'lodash';
import { MODE } from '../constants.js';
const FILTER_INDICATOR_LABEL = 'Filters:'; const FILTER_INDICATOR_LABEL = 'Filters:';
const FILTER_INDICATOR_LABEL_MIXED = 'Mixed Filters:'; const FILTER_INDICATOR_LABEL_MIXED = 'Mixed Filters:';
const FILTER_INDICATOR_TITLE = 'Data filters are being applied to this view.'; const FILTER_INDICATOR_TITLE = 'Data filters are being applied to this view.';
@ -81,7 +83,7 @@ export default {
}, },
telemetryMode: { telemetryMode: {
type: String, type: String,
default: 'performance' default: MODE.PERFORMANCE
} }
}, },
emits: ['telemetry-mode-change'], emits: ['telemetry-mode-change'],
@ -103,7 +105,7 @@ export default {
}); });
}, },
isUnlimitedMode() { isUnlimitedMode() {
return this.telemetryMode === 'unlimited'; return this.telemetryMode === MODE.UNLIMITED;
}, },
label() { label() {
if (this.hasMixedFilters) { if (this.hasMixedFilters) {

View File

@ -0,0 +1,11 @@
const ORDER = {
ASCENDING: 'asc',
DESCENDING: 'desc'
};
const MODE = {
PERFORMANCE: 'performance',
UNLIMITED: 'unlimited'
};
export { MODE, ORDER };

View File

@ -20,13 +20,14 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { MODE } from './constants.js';
import TableConfigurationViewProvider from './TableConfigurationViewProvider.js'; import TableConfigurationViewProvider from './TableConfigurationViewProvider.js';
import getTelemetryTableType from './TelemetryTableType.js'; import getTelemetryTableType from './TelemetryTableType.js';
import TelemetryTableViewProvider from './TelemetryTableViewProvider.js'; import TelemetryTableViewProvider from './TelemetryTableViewProvider.js';
import TelemetryTableViewActions from './ViewActions.js'; import TelemetryTableViewActions from './ViewActions.js';
export default function plugin( export default function plugin(
options = { telemetryMode: 'performance', persistModeChange: true, rowLimit: 50 } options = { telemetryMode: MODE.PERFORMANCE, persistModeChange: true, rowLimit: 50 }
) { ) {
return function install(openmct) { return function install(openmct) {
openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct, options)); openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct, options));

View File

@ -28,6 +28,7 @@ import {
} from 'utils/testing'; } from 'utils/testing';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { MODE } from './constants.js';
import TablePlugin from './plugin.js'; import TablePlugin from './plugin.js';
class MockDataTransfer { class MockDataTransfer {
@ -198,7 +199,7 @@ describe('the plugin', () => {
}, },
persistModeChange: true, persistModeChange: true,
rowLimit: 50, rowLimit: 50,
telemetryMode: 'performance' telemetryMode: MODE.PERFORMANCE
} }
}; };
const testTelemetry = [ const testTelemetry = [

View File

@ -41,21 +41,16 @@ import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
import utcMultiTimeFormat from './utcMultiTimeFormat.js'; import utcMultiTimeFormat from './utcMultiTimeFormat.js';
const PADDING = 1; const PADDING = 1;
const DEFAULT_DURATION_FORMATTER = 'duration';
const PIXELS_PER_TICK = 100; const PIXELS_PER_TICK = 100;
const PIXELS_PER_TICK_WIDE = 200; const PIXELS_PER_TICK_WIDE = 200;
export default { export default {
inject: ['openmct'], inject: ['openmct', 'isFixedTimeMode'],
props: { props: {
viewBounds: { viewBounds: {
type: Object, type: Object,
required: true required: true
}, },
isFixed: {
type: Boolean,
required: true
},
altPressed: { altPressed: {
type: Boolean, type: Boolean,
required: true required: true
@ -198,22 +193,8 @@ export default {
this.axisElement.call(this.xAxis); this.axisElement.call(this.xAxis);
this.setScale(); this.setScale();
}, },
getActiveFormatter() {
let timeSystem = this.openmct.time.getTimeSystem();
if (this.isFixed) {
return this.getFormatter(timeSystem.timeFormat);
} else {
return this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
}
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
},
dragStart($event) { dragStart($event) {
if (this.isFixed) { if (this.isFixedTimeMode) {
this.dragStartX = $event.clientX; this.dragStartX = $event.clientX;
if (this.altPressed) { if (this.altPressed) {

View File

@ -23,8 +23,8 @@
<div v-if="readOnly === false" ref="clockButton" class="c-tc-input-popup__options"> <div v-if="readOnly === false" ref="clockButton" class="c-tc-input-popup__options">
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left"> <div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button <button
class="c-button--menu js-clock-button" class="c-button--menu js-clock-button c-icon-button"
:class="[buttonCssClass, selectedClock.cssClass]" :class="selectedClock.cssClass"
aria-label="Time Conductor Clock Menu" aria-label="Time Conductor Clock Menu"
@click.prevent.stop="showClocksMenu" @click.prevent.stop="showClocksMenu"
> >
@ -38,18 +38,8 @@
</template> </template>
<script> <script>
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
import clockMixin from './clock-mixin.js';
export default { export default {
mixins: [clockMixin], inject: ['openmct', 'configuration', 'clock', 'getAllClockMetadata', 'getClockMetadata'],
inject: {
openmct: 'openmct',
configuration: {
from: 'configuration',
default: undefined
}
},
props: { props: {
readOnly: { readOnly: {
type: Boolean, type: Boolean,
@ -58,21 +48,13 @@ export default {
} }
} }
}, },
emits: ['clock-updated'], computed: {
data() { selectedClock() {
const activeClock = this.getActiveClock(); return this.getClockMetadata(this.clock);
}
return {
selectedClock: activeClock ? this.getClockMetadata(activeClock) : undefined,
clocks: []
};
}, },
mounted() { mounted() {
this.loadClocks(this.configuration.menuOptions); this.clocks = this.getAllClockMetadata(this.configuration.menuOptions);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
},
unmounted() {
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
}, },
methods: { methods: {
showClocksMenu() { showClocksMenu() {
@ -86,52 +68,6 @@ export default {
}; };
this.dismiss = this.openmct.menus.showSuperMenu(x, y, this.clocks, menuOptions); this.dismiss = this.openmct.menus.showSuperMenu(x, y, this.clocks, menuOptions);
},
setClock(clockKey) {
const option = {
clockKey
};
let configuration = this.getMatchingConfig({
clock: clockKey,
timeSystem: this.openmct.time.getTimeSystem().key
});
if (configuration === undefined) {
configuration = this.getMatchingConfig({
clock: clockKey
});
option.timeSystem = configuration.timeSystem;
option.bounds = configuration.bounds;
// this.openmct.time.setTimeSystem(configuration.timeSystem, configuration.bounds);
}
const offsets = this.openmct.time.getClockOffsets() ?? configuration.clockOffsets;
option.offsets = offsets;
this.$emit('clock-updated', option);
},
getMatchingConfig(options) {
const matchers = {
clock(config) {
return options.clock === config.clock;
},
timeSystem(config) {
return options.timeSystem === config.timeSystem;
}
};
function configMatches(config) {
return Object.keys(options).reduce((match, option) => {
return match && matchers[option](config);
}, true);
}
return this.configuration.menuOptions.filter(configMatches)[0];
},
setViewFromClock(clock) {
this.selectedClock = this.getClockMetadata(clock);
} }
} }
}; };

View File

@ -27,62 +27,50 @@
{ 'is-zooming': isZooming }, { 'is-zooming': isZooming },
{ 'is-panning': isPanning }, { 'is-panning': isPanning },
{ 'alt-pressed': altPressed }, { 'alt-pressed': altPressed },
isFixed ? 'is-fixed-mode' : 'is-realtime-mode' isFixedTimeMode ? 'is-fixed-mode' : 'is-realtime-mode'
]" ]"
> >
<ConductorModeIcon class="c-conductor__mode-icon" /> <ConductorModeIcon class="c-conductor__mode-icon" />
<div class="c-compact-tc__setting-value u-fade-truncate"> <div class="c-compact-tc__setting-value u-fade-truncate">
<ConductorMode :mode="mode" :read-only="true" /> <ConductorMode :read-only="true" />
<ConductorClock :read-only="true" /> <ConductorClock :read-only="true" />
<ConductorTimeSystem :read-only="true" /> <ConductorTimeSystem :read-only="true" />
</div> </div>
<ConductorInputsFixed v-if="isFixed" :input-bounds="viewBounds" :read-only="true" /> <ConductorInputsFixed v-if="isFixedTimeMode" :input-bounds="viewBounds" :read-only="true" />
<ConductorInputsRealtime v-else :input-bounds="viewBounds" :read-only="true" /> <ConductorInputsRealtime v-else :input-bounds="viewBounds" :read-only="true" />
<ConductorAxis <ConductorAxis
v-if="isFixed" v-if="isFixedTimeMode"
class="c-conductor__ticks" class="c-conductor__ticks"
:view-bounds="viewBounds" :view-bounds="viewBounds"
:is-fixed="isFixed"
:alt-pressed="altPressed" :alt-pressed="altPressed"
@end-pan="endPan" @end-pan="endPan"
@end-zoom="endZoom" @end-zoom="endZoom"
@pan-axis="pan" @pan-axis="pan"
@zoom-axis="zoom" @zoom-axis="zoom"
/> />
<div <ConductorHistory
role="button" v-if="!isIndependent"
class="c-not-button c-not-button--compact c-compact-tc__gear icon-gear" class="c-conductor__history-select"
aria-label="Time Conductor Settings" title="Select and apply previously entered time intervals."
></div> />
<ConductorPopUp <ConductorPopUp
v-if="showConductorPopup" v-if="showConductorPopup"
ref="conductorPopup" ref="conductorPopup"
:bottom="false" :bottom="false"
:position-x="positionX" :position-x="positionX"
:position-y="positionY" :position-y="positionY"
:is-fixed="isFixed"
@popup-loaded="initializePopup" @popup-loaded="initializePopup"
@mode-updated="saveMode"
@clock-updated="saveClock"
@fixed-bounds-updated="saveFixedBounds"
@clock-offsets-updated="saveClockOffsets"
@dismiss="clearPopup" @dismiss="clearPopup"
/> />
</div> </div>
</template> </template>
<script> <script>
import _ from 'lodash'; import { inject, provide } from 'vue';
import {
FIXED_MODE_KEY,
MODES,
REALTIME_MODE_KEY,
TIME_CONTEXT_EVENTS
} from '../../api/time/constants.js';
import ConductorAxis from './ConductorAxis.vue'; import ConductorAxis from './ConductorAxis.vue';
import ConductorClock from './ConductorClock.vue'; import ConductorClock from './ConductorClock.vue';
import ConductorHistory from './ConductorHistory.vue';
import ConductorInputsFixed from './ConductorInputsFixed.vue'; import ConductorInputsFixed from './ConductorInputsFixed.vue';
import ConductorInputsRealtime from './ConductorInputsRealtime.vue'; import ConductorInputsRealtime from './ConductorInputsRealtime.vue';
import ConductorMode from './ConductorMode.vue'; import ConductorMode from './ConductorMode.vue';
@ -90,14 +78,19 @@ import ConductorModeIcon from './ConductorModeIcon.vue';
import ConductorPopUp from './ConductorPopUp.vue'; import ConductorPopUp from './ConductorPopUp.vue';
import conductorPopUpManager from './conductorPopUpManager.js'; import conductorPopUpManager from './conductorPopUpManager.js';
import ConductorTimeSystem from './ConductorTimeSystem.vue'; import ConductorTimeSystem from './ConductorTimeSystem.vue';
import { useClock } from './useClock.js';
const DEFAULT_DURATION_FORMATTER = 'duration'; import { useClockOffsets } from './useClockOffsets.js';
import { useTimeBounds } from './useTimeBounds.js';
import { useTimeContext } from './useTimeContext.js';
import { useTimeMode } from './useTimeMode.js';
import { useTimeSystem } from './useTimeSystem.js';
export default { export default {
components: { components: {
ConductorTimeSystem, ConductorTimeSystem,
ConductorClock, ConductorClock,
ConductorMode, ConductorMode,
ConductorHistory,
ConductorInputsRealtime, ConductorInputsRealtime,
ConductorInputsFixed, ConductorInputsFixed,
ConductorAxis, ConductorAxis,
@ -106,38 +99,50 @@ export default {
}, },
mixins: [conductorPopUpManager], mixins: [conductorPopUpManager],
inject: ['openmct', 'configuration'], inject: ['openmct', 'configuration'],
data() { setup(props) {
const isFixed = this.openmct.time.isFixed(); const openmct = inject('openmct');
const bounds = this.openmct.time.getBounds(); const { timeContext } = useTimeContext(openmct);
const offsets = this.openmct.time.getClockOffsets(); const {
const timeSystem = this.openmct.time.getTimeSystem(); timeSystemKey,
const timeFormatter = this.getFormatter(timeSystem.timeFormat); timeSystemFormatter,
const durationFormatter = this.getFormatter( timeSystemDurationFormatter,
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER isTimeSystemUTCBased
); } = useTimeSystem(openmct, timeContext);
const { timeMode, isFixedTimeMode, isRealTimeMode, getAllModeMetadata, getModeMetadata } =
useTimeMode(openmct, timeContext);
const { bounds, isTick } = useTimeBounds(openmct, timeContext);
const { clock, getAllClockMetadata, getClockMetadata } = useClock(openmct, timeContext);
const { offsets } = useClockOffsets(openmct, timeContext);
provide('timeSystemKey', timeSystemKey);
provide('timeSystemFormatter', timeSystemFormatter);
provide('timeSystemDurationFormatter', timeSystemDurationFormatter);
provide('isTimeSystemUTCBased', isTimeSystemUTCBased);
provide('timeContext', timeContext);
provide('timeMode', timeMode);
provide('isFixedTimeMode', isFixedTimeMode);
provide('isRealTimeMode', isRealTimeMode);
provide('getAllModeMetadata', getAllModeMetadata);
provide('getModeMetadata', getModeMetadata);
provide('bounds', bounds);
provide('isTick', isTick);
provide('offsets', offsets);
provide('clock', clock);
provide('getAllClockMetadata', getAllClockMetadata);
provide('getClockMetadata', getClockMetadata);
return { return {
timeSystem, timeSystemFormatter,
timeFormatter, isFixedTimeMode,
durationFormatter, bounds
offsets: { };
start: offsets && durationFormatter.format(Math.abs(offsets.start)),
end: offsets && durationFormatter.format(Math.abs(offsets.end))
},
bounds: {
start: bounds.start,
end: bounds.end
},
formattedBounds: {
start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end)
}, },
data() {
return {
viewBounds: { viewBounds: {
start: bounds.start, start: this.bounds.start,
end: bounds.end end: this.bounds.end
}, },
isFixed,
isUTCBased: timeSystem.isUTCBased,
showDatePicker: false, showDatePicker: false,
showConductorPopup: false, showConductorPopup: false,
altPressed: false, altPressed: false,
@ -146,38 +151,30 @@ export default {
}; };
}, },
computed: { computed: {
mode() { formattedBounds() {
return this.isFixed ? FIXED_MODE_KEY : REALTIME_MODE_KEY; return {
start: this.timeSystemFormatter.format(this.bounds.start),
end: this.timeSystemFormatter.format(this.bounds.end)
};
}
},
watch: {
bounds: {
handler() {
this.setViewBounds(this.bounds);
},
deep: true
} }
}, },
mounted() { mounted() {
document.addEventListener('keydown', this.handleKeyDown); document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp); document.addEventListener('keyup', this.handleKeyUp);
this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
this.openmct.time.on(TIME_CONTEXT_EVENTS.boundsChanged, _.throttle(this.handleNewBounds, 300));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.openmct.time.on(TIME_CONTEXT_EVENTS.modeChanged, this.setMode);
}, },
beforeUnmount() { beforeUnmount() {
document.removeEventListener('keydown', this.handleKeyDown); document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp); document.removeEventListener('keyup', this.handleKeyUp);
this.openmct.time.off(TIME_CONTEXT_EVENTS.boundsChanged, _.throttle(this.handleNewBounds, 300));
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.openmct.time.off(TIME_CONTEXT_EVENTS.modeChanged, this.setMode);
}, },
methods: { methods: {
handleNewBounds(bounds, isTick) {
if (this.openmct.time.isRealTime() || !isTick) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
}
},
setBounds(bounds) {
this.bounds = bounds;
},
handleKeyDown(event) { handleKeyDown(event) {
if (event.key === 'Alt') { if (event.key === 'Alt') {
this.altPressed = true; this.altPressed = true;
@ -190,7 +187,7 @@ export default {
}, },
pan(bounds) { pan(bounds) {
this.isPanning = true; this.isPanning = true;
this.setViewFromBounds(bounds); this.setViewBounds(bounds);
}, },
endPan(bounds) { endPan(bounds) {
this.isPanning = false; this.isPanning = false;
@ -203,8 +200,8 @@ export default {
this.isZooming = false; this.isZooming = false;
} else { } else {
this.isZooming = true; this.isZooming = true;
this.formattedBounds.start = this.timeFormatter.format(bounds.start); this.formattedBounds.start = this.timeSystemFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end); this.formattedBounds.end = this.timeSystemFormatter.format(bounds.end);
} }
}, },
endZoom(bounds) { endZoom(bounds) {
@ -212,49 +209,12 @@ export default {
if (bounds) { if (bounds) {
this.openmct.time.setBounds(bounds); this.openmct.time.setBounds(bounds);
} else { } else {
this.setViewFromBounds(this.bounds); this.setViewBounds(this.bounds);
} }
}, },
setTimeSystem(timeSystem) { setViewBounds(bounds) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
this.isUTCBased = timeSystem.isUTCBased;
},
setMode() {
this.isFixed = this.openmct.time.isFixed();
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
this.viewBounds.start = bounds.start; this.viewBounds.start = bounds.start;
this.viewBounds.end = bounds.end; this.viewBounds.end = bounds.end;
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
},
getBoundsForMode(mode) {
const isRealTime = mode === MODES.realtime;
return isRealTime ? this.openmct.time.getClockOffsets() : this.openmct.time.getBounds();
},
saveFixedBounds(bounds) {
this.openmct.time.setBounds(bounds);
},
saveClockOffsets(offsets) {
this.openmct.time.setClockOffsets(offsets);
},
saveClock(clockOptions) {
this.openmct.time.setClock(clockOptions.clockKey);
},
saveMode(mode) {
this.openmct.time.setMode(mode, this.getBoundsForMode(mode));
},
copy(object) {
return JSON.parse(JSON.stringify(object));
} }
} }
}; };

View File

@ -24,12 +24,9 @@
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left"> <div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button <button
aria-label="Time Conductor History" aria-label="Time Conductor History"
class="c-button--menu c-history-button icon-history" class="c-button--minor c-history-button icon-history c-icon-button"
:class="buttonCssClass"
@click.prevent.stop="showHistoryMenu" @click.prevent.stop="showHistoryMenu"
> />
<span class="c-button__label">History</span>
</button>
</div> </div>
</div> </div>
</template> </template>
@ -47,15 +44,6 @@ import UTCTimeFormat from '../utcTimeSystem/UTCTimeFormat.js';
export default { export default {
inject: ['openmct', 'configuration'], inject: ['openmct', 'configuration'],
props: {
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
}
},
data() { data() {
const mode = this.openmct.time.getMode(); const mode = this.openmct.time.getMode();

View File

@ -20,14 +20,8 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<template> <template>
<TimePopupFixed <DateTimePopupFixed v-if="delimiter && !readOnly" :delimiter="delimiter" @focus="$event.target.select()" @dismiss="dismiss" />
v-if="readOnly === false" <TimePopupFixed v-else-if="!readOnly" @focus="$event.target.select()" @dismiss="dismiss" />
:input-bounds="bounds"
:input-time-system="timeSystem"
@focus="$event.target.select()"
@update="setBoundsFromView"
@dismiss="dismiss"
/>
<div v-else class="c-compact-tc__setting-wrapper"> <div v-else class="c-compact-tc__setting-wrapper">
<div <div
class="c-compact-tc__setting-value u-fade-truncate--lg --no-sep" class="c-compact-tc__setting-value u-fade-truncate--lg --no-sep"
@ -48,29 +42,16 @@
</template> </template>
<script> <script>
import _ from 'lodash';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
import TimePopupFixed from './TimePopupFixed.vue'; import TimePopupFixed from './TimePopupFixed.vue';
import DateTimePopupFixed from './DateTimePopupFixed.vue';
export default { export default {
components: { components: {
TimePopupFixed TimePopupFixed,
DateTimePopupFixed
}, },
inject: ['openmct'], inject: ['openmct', 'timeContext', 'bounds', 'timeSystemFormatter'],
props: { props: {
inputBounds: {
type: Object,
default() {
return undefined;
}
},
objectPath: {
type: Array,
default() {
return [];
}
},
readOnly: { readOnly: {
type: Boolean, type: Boolean,
default() { default() {
@ -84,94 +65,19 @@ export default {
} }
} }
}, },
emits: ['bounds-updated', 'dismiss-inputs-fixed'], emits: ['dismiss-inputs-fixed'],
data() { computed: {
const timeSystem = this.openmct.time.getTimeSystem(); delimiter() {
const timeFormatter = this.getFormatter(timeSystem.timeFormat); return this.timeSystemFormatter.getDelimiter?.();
let bounds = this.inputBounds || this.openmct.time.getBounds(); },
formattedBounds() {
return { return {
timeSystem, start: this.timeSystemFormatter.format(this.bounds.start),
timeFormatter, end: this.timeSystemFormatter.format(this.bounds.end)
bounds: {
start: bounds.start,
end: bounds.end
},
formattedBounds: {
start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end)
},
isUTCBased: timeSystem.isUTCBased
}; };
},
watch: {
objectPath: {
handler(newPath, oldPath) {
if (newPath === oldPath) {
return;
} }
this.setTimeContext();
},
deep: true
},
inputBounds: {
handler(newBounds) {
this.handleNewBounds(newBounds);
},
deep: true
}
},
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem())));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.setTimeContext();
},
beforeUnmount() {
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.stopFollowingTimeContext();
}, },
methods: { methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.handleNewBounds(this.timeContext.getBounds());
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
}
},
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
},
setBounds(bounds) {
this.bounds = bounds;
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
},
setTimeSystem(timeSystem) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.isUTCBased = timeSystem.isUTCBased;
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
},
setBoundsFromView(bounds) {
this.$emit('bounds-updated', {
start: bounds.start,
end: bounds.end
});
},
dismiss() { dismiss() {
this.$emit('dismiss-inputs-fixed'); this.$emit('dismiss-inputs-fixed');
} }

View File

@ -22,29 +22,28 @@
<template> <template>
<TimePopupRealtime <TimePopupRealtime
v-if="readOnly === false" v-if="readOnly === false"
:offsets="offsets" :offsets="formattedOffsets"
@focus="$event.target.select()" @focus="$event.target.select()"
@update="timePopUpdate"
@dismiss="dismiss" @dismiss="dismiss"
/> />
<div v-else class="c-compact-tc__setting-wrapper"> <div v-else class="c-compact-tc__setting-wrapper">
<div <div
v-if="!compact" v-if="!compact"
class="c-compact-tc__setting-value icon-minus u-fade-truncate--lg --no-sep" class="c-compact-tc__setting-value icon-minus u-fade-truncate--lg --no-sep"
:aria-label="`Start offset: ${offsets.start}`" :aria-label="`Start offset: ${formattedOffsets.start}`"
:title="`Start offset: ${offsets.start}`" :title="`Start offset: ${formattedOffsets.start}`"
> >
{{ offsets.start }} {{ formattedOffsets.start }}
</div> </div>
<div v-if="!compact" class="c-compact-tc__bounds__start-end-sep icon-arrows-right-left"></div> <div v-if="!compact" class="c-compact-tc__bounds__start-end-sep icon-arrows-right-left"></div>
<div <div
v-if="!compact" v-if="!compact"
class="c-compact-tc__setting-value icon-plus u-fade-truncate--lg" class="c-compact-tc__setting-value icon-plus u-fade-truncate--lg"
:class="{ '--no-sep': compact }" :class="{ '--no-sep': compact }"
:aria-label="`End offset: ${offsets.end}`" :aria-label="`End offset: ${formattedOffsets.end}`"
:title="`End offset: ${offsets.end}`" :title="`End offset: ${formattedOffsets.end}`"
> >
{{ offsets.end }} {{ formattedOffsets.end }}
</div> </div>
<div <div
class="c-compact-tc__setting-value icon-clock c-compact-tc__current-update u-fade-truncate--lg --no-sep" class="c-compact-tc__setting-value icon-clock c-compact-tc__current-update u-fade-truncate--lg --no-sep"
@ -58,31 +57,21 @@
</template> </template>
<script> <script>
import _ from 'lodash';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
import TimePopupRealtime from './TimePopupRealtime.vue'; import TimePopupRealtime from './TimePopupRealtime.vue';
const DEFAULT_DURATION_FORMATTER = 'duration';
export default { export default {
components: { components: {
TimePopupRealtime TimePopupRealtime
}, },
inject: ['openmct'], inject: [
'openmct',
'bounds',
'clock',
'offsets',
'timeSystemFormatter',
'timeSystemDurationFormatter'
],
props: { props: {
objectPath: {
type: Array,
default() {
return [];
}
},
inputBounds: {
type: Object,
default() {
return undefined;
}
},
readOnly: { readOnly: {
type: Boolean, type: Boolean,
default() { default() {
@ -96,167 +85,34 @@ export default {
} }
} }
}, },
emits: ['offsets-updated', 'dismiss-inputs-realtime'], emits: ['dismiss-inputs-realtime'],
data() { data() {
const timeSystem = this.openmct.time.getTimeSystem();
const durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
const timeFormatter = this.getFormatter(timeSystem.timeFormat);
const bounds = this.bounds ?? this.openmct.time.getBounds();
const offsets = this.offsets ?? this.openmct.time.getClockOffsets();
const currentValue = this.openmct.time.getClock()?.currentValue();
return { return {
showTCInputStart: false, currentValue: this.clock.currentValue()
showTCInputEnd: false,
durationFormatter,
timeFormatter,
bounds: {
start: bounds.start,
end: bounds.end
},
offsets: {
start: offsets && durationFormatter.format(Math.abs(offsets.start)),
end: offsets && durationFormatter.format(Math.abs(offsets.end))
},
formattedBounds: {
start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end)
},
currentValue,
formattedCurrentValue: timeFormatter.format(currentValue),
isUTCBased: timeSystem.isUTCBased
}; };
}, },
computed: {
formattedOffsets() {
return {
start: this.timeSystemDurationFormatter.format(Math.abs(this.offsets.start)),
end: this.timeSystemDurationFormatter.format(Math.abs(this.offsets.end))
};
},
formattedCurrentValue() {
return this.timeSystemFormatter.format(this.currentValue);
}
},
watch: { watch: {
objectPath: { bounds() {
handler(newPath, oldPath) {
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
},
inputBounds: {
handler(newBounds) {
this.handleNewBounds(newBounds);
},
deep: true
}
},
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300, {
leading: true,
trailing: false
});
this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.setTimeContext();
},
beforeUnmount() {
this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.stopFollowingTime();
},
methods: {
followTime() {
const bounds = this.timeContext
? this.timeContext.getBounds()
: this.openmct.time.getBounds();
const offsets = this.timeContext
? this.timeContext.getClockOffsets()
: this.openmct.time.getClockOffsets();
this.handleNewBounds(bounds);
this.setViewFromOffsets(offsets);
if (this.timeContext) {
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.timeContext.on(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
} else {
this.openmct.time.on(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
}
},
stopFollowingTime() {
if (this.timeContext) {
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.timeContext.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
} else {
this.openmct.time.off(TIME_CONTEXT_EVENTS.boundsChanged, this.handleNewBounds);
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.setViewFromOffsets);
}
},
setTimeContext() {
this.stopFollowingTime();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.followTime();
},
handleNewBounds(bounds, isTick) {
if (this.timeContext.isRealTime() || !isTick) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
this.updateCurrentValue(); this.updateCurrentValue();
} }
}, },
setViewFromOffsets(offsets) { methods: {
if (offsets) {
this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start));
this.offsets.end = this.durationFormatter.format(Math.abs(offsets.end));
}
},
setBounds(bounds) {
this.bounds = bounds;
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
},
updateCurrentValue() { updateCurrentValue() {
const currentValue = this.timeContext.getClock().currentValue(); this.currentValue = this.clock.currentValue();
if (currentValue !== undefined) {
this.setCurrentValue(currentValue);
}
},
setCurrentValue(value) {
this.currentValue = value;
this.formattedCurrentValue = this.timeFormatter.format(value);
},
setTimeSystem(timeSystem) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
this.isUTCBased = timeSystem.isUTCBased;
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
},
timePopUpdate({ start, end }) {
this.offsets.start = [start.hours, start.minutes, start.seconds].join(':');
this.offsets.end = [end.hours, end.minutes, end.seconds].join(':');
this.setOffsetsFromView();
},
setOffsetsFromView() {
let startOffset = 0 - this.durationFormatter.parse(this.offsets.start);
let endOffset = this.durationFormatter.parse(this.offsets.end);
this.$emit('offsets-updated', {
start: startOffset,
end: endOffset
});
}, },
dismiss() { dismiss() {
this.$emit('dismiss-inputs-realtime'); this.$emit('dismiss-inputs-realtime');
},
copy(object) {
return JSON.parse(JSON.stringify(object));
} }
} }
}; };

View File

@ -23,8 +23,8 @@
<div v-if="readOnly === false" ref="modeButton" class="c-tc-input-popup__options"> <div v-if="readOnly === false" ref="modeButton" class="c-tc-input-popup__options">
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left"> <div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button <button
class="c-button--menu js-mode-button" class="c-button--menu js-mode-button c-icon-button"
:class="[buttonCssClass, selectedMode.cssClass]" :class="selectedMode.cssClass"
aria-label="Time Conductor Mode Menu" aria-label="Time Conductor Mode Menu"
@click.prevent.stop="showModesMenu" @click.prevent.stop="showModesMenu"
> >
@ -43,20 +43,9 @@
</template> </template>
<script> <script>
import modeMixin from './mode-mixin.js';
const TEST_IDS = true;
export default { export default {
mixins: [modeMixin], inject: ['openmct', 'timeMode', 'getAllModeMetadata', 'getModeMetadata'],
inject: ['openmct', 'configuration'],
props: { props: {
mode: {
type: String,
default() {
return undefined;
}
},
readOnly: { readOnly: {
type: Boolean, type: Boolean,
default() { default() {
@ -64,24 +53,20 @@ export default {
} }
} }
}, },
emits: ['mode-updated'],
data() { data() {
const mode = this.openmct.time.getMode();
return { return {
selectedMode: this.getModeMetadata(mode, TEST_IDS), selectedMode: this.getModeMetadata(this.timeMode)
modes: []
}; };
}, },
watch: { watch: {
mode: { timeMode: {
handler(newMode) { handler() {
this.setViewFromMode(newMode); this.setView();
} }
} }
}, },
mounted() { mounted() {
this.loadModes(); this.modes = this.getAllModeMetadata();
}, },
methods: { methods: {
showModesMenu() { showModesMenu() {
@ -96,13 +81,8 @@ export default {
this.dismiss = this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions); this.dismiss = this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);
}, },
setViewFromMode(mode) { setView() {
this.selectedMode = this.getModeMetadata(mode, TEST_IDS); this.selectedMode = this.getModeMetadata(this.timeMode);
},
setMode(mode) {
this.setViewFromMode(mode);
this.$emit('mode-updated', mode);
} }
} }
}; };

View File

@ -5,66 +5,36 @@
v-if="isIndependent" v-if="isIndependent"
class="c-conductor__mode-select" class="c-conductor__mode-select"
title="Sets the Time Conductor's mode." title="Sets the Time Conductor's mode."
:mode="timeOptionMode"
@independent-mode-updated="saveIndependentMode"
/> />
<ConductorMode <ConductorMode
v-else v-else
class="c-conductor__mode-select" class="c-conductor__mode-select"
title="Sets the Time Conductor's mode." title="Sets the Time Conductor's mode."
:button-css-class="'c-icon-button'"
@mode-updated="saveMode"
/> />
<IndependentClock <IndependentClock
v-if="isIndependent" v-if="isIndependent"
class="c-conductor__mode-select" class="c-conductor__mode-select"
title="Sets the Time Conductor's clock." title="Sets the Time Conductor's clock."
:clock="timeOptionClock"
:button-css-class="'c-icon-button'"
@independent-clock-updated="saveIndependentClock"
/> />
<ConductorClock <ConductorClock
v-else v-else
class="c-conductor__mode-select" class="c-conductor__mode-select"
title="Sets the Time Conductor's clock." title="Sets the Time Conductor's clock."
:button-css-class="'c-icon-button'"
@clock-updated="saveClock"
/> />
<!-- TODO: Time system and history must work even with ITC later --> <!-- TODO: Time system and history must work even with ITC later -->
<ConductorTimeSystem <ConductorTimeSystem
v-if="!isIndependent" v-if="!isIndependent"
class="c-conductor__time-system-select" class="c-conductor__time-system-select"
title="Sets the Time Conductor's time system." title="Sets the Time Conductor's time system."
:button-css-class="'c-icon-button'"
/>
<ConductorHistory
v-if="!isIndependent"
class="c-conductor__history-select"
title="Select and apply previously entered time intervals."
:button-css-class="'c-icon-button'"
/> />
</div> </div>
<ConductorInputsFixed <ConductorInputsFixed v-if="isFixedTimeMode" @dismiss-inputs-fixed="dismiss" />
v-if="isFixed" <ConductorInputsRealtime v-else @dismiss-inputs-realtime="dismiss" />
:input-bounds="bounds"
:object-path="objectPath"
@bounds-updated="saveFixedBounds"
@dismiss-inputs-fixed="dismiss"
/>
<ConductorInputsRealtime
v-else
:input-bounds="bounds"
:object-path="objectPath"
@offsets-updated="saveClockOffsets"
@dismiss-inputs-realtime="dismiss"
/>
</div> </div>
</template> </template>
<script> <script>
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
import ConductorClock from './ConductorClock.vue'; import ConductorClock from './ConductorClock.vue';
import ConductorHistory from './ConductorHistory.vue';
import ConductorInputsFixed from './ConductorInputsFixed.vue'; import ConductorInputsFixed from './ConductorInputsFixed.vue';
import ConductorInputsRealtime from './ConductorInputsRealtime.vue'; import ConductorInputsRealtime from './ConductorInputsRealtime.vue';
import ConductorMode from './ConductorMode.vue'; import ConductorMode from './ConductorMode.vue';
@ -79,17 +49,10 @@ export default {
IndependentMode, IndependentMode,
IndependentClock, IndependentClock,
ConductorTimeSystem, ConductorTimeSystem,
ConductorHistory,
ConductorInputsFixed, ConductorInputsFixed,
ConductorInputsRealtime ConductorInputsRealtime
}, },
inject: { inject: ['openmct', 'isFixedTimeMode'],
openmct: 'openmct',
configuration: {
from: 'configuration',
default: undefined
}
},
props: { props: {
positionX: { positionX: {
type: Number, type: Number,
@ -99,57 +62,20 @@ export default {
type: Number, type: Number,
required: true required: true
}, },
isFixed: {
type: Boolean,
required: true
},
isIndependent: { isIndependent: {
type: Boolean, type: Boolean,
default() { default() {
return false; return false;
} }
}, },
timeOptions: {
type: Object,
default() {
return undefined;
}
},
bottom: { bottom: {
type: Boolean, type: Boolean,
default() { default() {
return false; return false;
} }
},
objectPath: {
type: Array,
default() {
return [];
}
} }
}, },
emits: [ emits: ['popup-loaded', 'dismiss'],
'popup-loaded',
'dismiss',
'independent-clock-updated',
'fixed-bounds-updated',
'clock-offsets-updated',
'clock-updated',
'mode-updated',
'independent-mode-updated'
],
data() {
const bounds = this.openmct.time.getBounds();
const timeSystem = this.openmct.time.getTimeSystem();
return {
timeSystem,
bounds: {
start: bounds.start,
end: bounds.end
}
};
},
computed: { computed: {
position() { position() {
const position = { const position = {
@ -164,84 +90,16 @@ export default {
}, },
popupClasses() { popupClasses() {
const value = this.bottom ? 'c-tc-input-popup--bottom ' : ''; const value = this.bottom ? 'c-tc-input-popup--bottom ' : '';
const mode = this.isFixed ? 'fixed-mode' : 'realtime-mode'; const mode = this.isFixedTimeMode ? 'fixed-mode' : 'realtime-mode';
const independentClass = this.isIndependent ? 'itc-popout ' : ''; const independentClass = this.isIndependent ? 'itc-popout ' : '';
return `${independentClass}${value}c-tc-input-popup--${mode}`; return `${independentClass}${value}c-tc-input-popup--${mode}`;
},
timeOptionMode() {
return this.timeOptions?.mode;
},
timeOptionClock() {
return this.timeOptions?.clock;
}
},
watch: {
objectPath: {
handler(newPath, oldPath) {
//domain object or view has probably changed
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
} }
}, },
mounted() { mounted() {
this.$emit('popup-loaded'); this.$emit('popup-loaded');
this.setTimeContext();
},
beforeUnmount() {
this.stopFollowingTimeContext();
}, },
methods: { methods: {
setTimeContext() {
if (this.timeContext) {
this.stopFollowingTimeContext();
}
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
this.timeContext.on(TIME_CONTEXT_EVENTS.boundsChanged, this.setBounds);
this.setViewFromClock(this.timeContext.getClock());
this.setBounds(this.timeContext.getBounds());
},
stopFollowingTimeContext() {
this.timeContext.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
this.timeContext.off(TIME_CONTEXT_EVENTS.boundsChanged, this.setBounds);
},
setViewFromClock() {
this.bounds = this.isFixed
? this.timeContext.getBounds()
: this.openmct.time.getClockOffsets();
},
setBounds(bounds, isTick) {
if (this.isFixed || !isTick) {
this.bounds = bounds;
}
},
saveFixedBounds(bounds) {
this.$emit('fixed-bounds-updated', bounds);
},
saveClockOffsets(offsets) {
this.$emit('clock-offsets-updated', offsets);
},
saveClock(clockOptions) {
this.$emit('clock-updated', clockOptions);
},
saveMode(mode) {
this.$emit('mode-updated', mode);
},
saveIndependentMode(mode) {
this.$emit('independent-mode-updated', mode);
},
saveIndependentClock(clockKey) {
this.$emit('independent-clock-updated', clockKey);
},
dismiss() { dismiss() {
this.$emit('dismiss'); this.$emit('dismiss');
} }

View File

@ -26,8 +26,7 @@
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up" class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
> >
<button <button
class="c-button--menu c-time-system-button" class="c-button--menu c-time-system-button c-icon-button"
:class="[buttonCssClass]"
aria-label="Time Conductor Time System" aria-label="Time Conductor Time System"
@click.prevent.stop="showTimeSystemMenu" @click.prevent.stop="showTimeSystemMenu"
> >
@ -50,13 +49,6 @@ import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
export default { export default {
inject: ['openmct', 'configuration'], inject: ['openmct', 'configuration'],
props: { props: {
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
},
readOnly: { readOnly: {
type: Boolean, type: Boolean,
default() { default() {

View File

@ -107,10 +107,6 @@ export default {
type: String, type: String,
default: undefined default: undefined
}, },
formatter: {
type: Object,
required: true
},
bottom: { bottom: {
type: Boolean, type: Boolean,
default() { default() {

View File

@ -0,0 +1,316 @@
<!--
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>
<form ref="fixedDeltaInput">
<div class="c-tc-input-popup__input-grid-utc">
<div class="pr-time-label pr-time-label-start-date"><em>Start</em> Date</div>
<div class="pr-time-label pr-time-label-start-time">Time</div>
<div class="pr-time-label pr-time-label-end-date"><em>End</em> Date</div>
<div class="pr-time-label pr-time-label-end-time">Time</div>
<div
class="pr-time-input pr-time-input--date pr-time-input--input-and-button pr-time-input-start-date"
>
<DatePicker
v-if="canSplitDateTime"
class="c-ctrl-wrapper--menus-right"
:default-date-time="formattedBounds.startDate"
@date-selected="startDateSelected"
/>
<input
ref="startDate"
v-model="formattedBounds.startDate"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
aria-label="Start date"
@input="validateInput('startDate')"
@change="reportValidity('startDate')"
/>
</div>
<div class="pr-time-input pr-time-input--time pr-time-input-start-time">
<input
ref="startTime"
v-model="formattedBounds.startTime"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
aria-label="Start time"
@input="validateInput('startTime')"
@change="reportValidity('startTime')"
/>
</div>
<div class="pr-time-input pr-time-input__start-end-sep icon-arrows-right-left"></div>
<div
class="pr-time-input pr-time-input--date pr-time-input--input-and-button pr-time-input-end-date"
>
<DatePicker
v-if="canSplitDateTime"
class="c-ctrl-wrapper--menus-left"
:default-date-time="formattedBounds.endDate"
@date-selected="endDateSelected"
/>
<input
ref="endDate"
v-model="formattedBounds.endDate"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
aria-label="End date"
@input="validateInput('endDate')"
@change="reportValidity('endDate')"
/>
</div>
<div class="pr-time-input pr-time-input--time pr-time-input-end-time">
<input
ref="endTime"
v-model="formattedBounds.endTime"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
aria-label="End time"
@input="validateInput('endTime')"
@change="reportValidity('endTime')"
/>
</div>
<div class="pr-time-input pr-time-input--buttons">
<button
class="c-button c-button--major icon-check"
:disabled="hasInputValidityError"
aria-label="Submit time bounds"
@click.prevent="handleFormSubmission(true)"
></button>
<button
class="c-button icon-x"
aria-label="Discard changes and close time popup"
@click.prevent="hide"
></button>
</div>
</div>
</form>
</template>
<script>
import DatePicker from './DatePicker.vue';
export default {
components: {
DatePicker
},
inject: [
'openmct',
'isTimeSystemUTCBased',
'timeContext',
'timeSystemKey',
'timeSystemFormatter',
'timeSystemDurationFormatter',
'bounds'
],
props: {
delimiter: {
type: String,
required: true
}
},
emits: ['dismiss'],
data() {
return {
formattedBounds: {},
inputValidityMap: {
startDate: { valid: true },
startTime: { valid: true },
endDate: { valid: true },
endTime: { valid: true }
},
logicalValidityMap: {
limit: { valid: true },
bounds: { valid: true }
}
};
},
computed: {
hasInputValidityError() {
return Object.values(this.inputValidityMap).some((isValid) => !isValid.valid);
},
hasLogicalValidationErrors() {
return Object.values(this.logicalValidityMap).some((isValid) => !isValid.valid);
},
isValid() {
return !this.hasInputValidityError && !this.hasLogicalValidationErrors;
}
},
watch: {
bounds: {
handler() {
this.setViewFromBounds();
}
}
},
mounted() {
this.setViewFromBounds();
},
beforeUnmount() {
this.clearAllValidation();
},
methods: {
setViewFromBounds() {
this.formattedBounds.startDate = this.timeSystemFormatter
.format(this.bounds.start)
.split(this.delimiter)[0];
this.formattedBounds.endDate = this.timeSystemFormatter
.format(this.bounds.end)
.split(this.delimiter)[0];
this.formattedBounds.startTime = this.timeSystemDurationFormatter.format(
Math.abs(this.bounds.start)
);
this.formattedBounds.endTime = this.timeSystemDurationFormatter.format(
Math.abs(this.bounds.end)
);
},
setBoundsFromView(dismiss) {
if (this.$refs.fixedDeltaInput.checkValidity()) {
const start = this.timeSystemFormatter.parse(
`${this.formattedBounds.startDate}${this.delimiter}${this.formattedBounds.startTime}`
);
const end = this.timeSystemFormatter.parse(
`${this.formattedBounds.endDate}${this.delimiter}${this.formattedBounds.endTime}`
);
this.timeContext.setBounds({
start,
end
});
}
if (dismiss) {
this.$emit('dismiss');
return false;
}
},
clearAllValidation() {
Object.keys(this.inputValidityMap).forEach(this.clearValidation);
},
clearValidation(refName) {
const input = this.getInput(refName);
input.setCustomValidity('');
input.title = '';
},
handleFormSubmission(shouldDismiss) {
this.validateLimit();
this.reportValidity('limit');
this.validateBounds();
this.reportValidity('bounds');
if (this.isValid) {
this.setBoundsFromView(shouldDismiss);
}
},
validateInput(refName) {
this.clearAllValidation();
const inputType = refName.includes('Date') ? 'Date' : 'Time';
const formatter = inputType === 'Date' ? this.timeSystemFormatter : this.durationFormatter;
const validationResult = formatter.validate(this.formattedBounds[refName])
? { valid: true }
: { valid: false, message: `Invalid ${inputType}` };
this.inputValidityMap[refName] = validationResult;
},
validateBounds() {
const bounds = {
start: this.timeSystemFormatter.parse(
`${this.formattedBounds.startDate}${this.delimiter}${this.formattedBounds.startTime}`
),
end: this.timeSystemFormatter.parse(
`${this.formattedBounds.endDate}${this.delimiter}${this.formattedBounds.endTime}`
)
};
this.logicalValidityMap.bounds = this.timeContext.validateBounds(bounds);
},
validateLimit(bounds) {
const limit = this.configuration?.menuOptions
?.filter((option) => option.timeSystem === this.timeSystemKey)
?.find((option) => option.limit)?.limit;
if (this.isTimeSystemUTCBased && limit && bounds.end - bounds.start > limit) {
this.logicalValidityMap.limit = {
valid: false,
message: 'Start and end difference exceeds allowable limit of ${limit}'
};
} else {
this.logicalValidityMap.limit = { valid: true };
}
},
reportValidity(refName) {
const input = this.getInput(refName);
const validationResult = this.inputValidityMap[refName] ?? this.logicalValidityMap[refName];
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
} else {
input.setCustomValidity('');
input.title = '';
}
this.$refs.fixedDeltaInput.reportValidity();
},
getInput(refName) {
if (Object.keys(this.inputValidityMap).includes(refName)) {
return this.$refs[refName];
}
return this.$refs.startDate;
},
startDateSelected(date) {
this.formattedBounds.startDate = this.timeSystemFormatter
.format(date)
.split(this.delimiter)[0];
this.validateInput('startDate');
this.reportValidity('startDate');
},
endDateSelected(date) {
this.formattedBounds.endDate = this.timeSystemFormatter.format(date).split(this.delimiter)[0];
this.validateInput('endDate');
this.reportValidity('endDate');
},
hide($event) {
if ($event.target.className.indexOf('c-button icon-x') > -1) {
this.$emit('dismiss');
}
}
}
};
</script>

View File

@ -1,87 +1,77 @@
<!--
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> <template>
<form ref="fixedDeltaInput"> <form ref="fixedDeltaInput">
<div class="c-tc-input-popup__input-grid"> <div class="c-tc-input-popup__input-grid">
<div class="pr-time-label pr-time-label-start-date"><em>Start</em> Date</div> <div class="pr-time-label pr-time-label-start-time">Start</div>
<div class="pr-time-label pr-time-label-start-time">Time</div> <div class="pr-time-label pr-time-label-end-time">End</div>
<div class="pr-time-label pr-time-label-end-date"><em>End</em> Date</div>
<div class="pr-time-label pr-time-label-end-time">Time</div>
<div <div class="pr-time-input pr-time-input-start">
class="pr-time-input pr-time-input--date pr-time-input--input-and-button pr-time-input-start-date"
>
<input <input
ref="startDate" ref="start"
v-model="formattedBounds.start" v-model="formattedBounds.start"
class="c-input--datetime" class="c-input--datetime"
type="text" type="text"
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
aria-label="Start date" aria-label="Start time"
@input="validateAllBounds('startDate')" @input="validateInput('start')"
@change="reportValidity('start')"
/> />
<DatePicker <DatePicker
v-if="isUTCBased" v-if="isTimeSystemUTCBased"
class="c-ctrl-wrapper--menus-right" class="c-ctrl-wrapper--menus-left"
:default-date-time="formattedBounds.start" :default-date-time="formattedBounds.start"
:formatter="timeFormatter" @date-selected="dateSelected($event, 'start')"
@date-selected="startDateSelected"
/>
</div>
<div class="pr-time-input pr-time-input--time pr-time-input-start-time">
<input
ref="startTime"
v-model="formattedBounds.startTime"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
aria-label="Start time"
@input="validateAllBounds('startDate')"
/> />
</div> </div>
<div class="pr-time-input pr-time-input__start-end-sep icon-arrows-right-left"></div> <div class="pr-time-input pr-time-input__start-end-sep icon-arrows-right-left"></div>
<div <div class="pr-time-input pr-time-input-end">
class="pr-time-input pr-time-input--date pr-time-input--input-and-button pr-time-input-end-date"
>
<input <input
ref="endDate" ref="end"
v-model="formattedBounds.end" v-model="formattedBounds.end"
class="c-input--datetime" class="c-input--datetime"
type="text" type="text"
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
aria-label="End date" aria-label="End time"
@input="validateAllBounds('endDate')" @input="validateInput('end')"
@change="reportValidity('end')"
/> />
<DatePicker <DatePicker
v-if="isUTCBased" v-if="isTimeSystemUTCBased"
class="c-ctrl-wrapper--menus-left" class="c-ctrl-wrapper--menus-left"
:default-date-time="formattedBounds.end" :default-date-time="formattedBounds.end"
:formatter="timeFormatter" @date-selected="dateSelected($event, 'end')"
@date-selected="endDateSelected"
/>
</div>
<div class="pr-time-input pr-time-input--time pr-time-input-end-time">
<input
ref="endTime"
v-model="formattedBounds.endTime"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
aria-label="End time"
@input="validateAllBounds('endDate')"
/> />
</div> </div>
<div class="pr-time-input pr-time-input--buttons"> <div class="pr-time-input pr-time-input--buttons">
<button <button
class="c-button c-button--major icon-check" class="c-button c-button--major icon-check"
:disabled="isDisabled" :disabled="hasInputValidityError"
aria-label="Submit time bounds" aria-label="Submit time bounds"
@click.prevent="handleFormSubmission(true)" @click.prevent="handleFormSubmission(true)"
></button> ></button>
@ -96,118 +86,79 @@
</template> </template>
<script> <script>
import _ from 'lodash';
import DatePicker from './DatePicker.vue'; import DatePicker from './DatePicker.vue';
const DEFAULT_DURATION_FORMATTER = 'duration';
export default { export default {
components: { components: {
DatePicker DatePicker
}, },
inject: ['openmct'], inject: [
props: { 'openmct',
inputBounds: { 'configuration',
type: Object, 'isTimeSystemUTCBased',
required: true 'timeContext',
}, 'timeSystemKey',
inputTimeSystem: { 'timeSystemFormatter',
type: Object, 'timeSystemDurationFormatter',
required: true 'bounds'
} ],
}, emits: ['dismiss'],
emits: ['update', 'dismiss'],
data() { data() {
const timeSystem = this.openmct.time.getTimeSystem();
const bounds = this.openmct.time.getBounds();
return { return {
timeFormatter: this.getFormatter(timeSystem.timeFormat), formattedBounds: {},
durationFormatter: this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER), inputValidityMap: {
bounds: { start: { valid: true },
start: bounds.start, end: { valid: true }
end: bounds.end
}, },
formattedBounds: { logicalValidityMap: {
start: '', limit: { valid: true },
end: '', bounds: { valid: true }
startTime: '', }
endTime: ''
},
isUTCBased: timeSystem.isUTCBased,
isDisabled: false
}; };
}, },
watch: { computed: {
inputBounds: { hasInputValidityError() {
handler(newBounds) { return Object.values(this.inputValidityMap).some((isValid) => !isValid.valid);
this.handleNewBounds(newBounds);
}, },
deep: true hasLogicalValidationErrors() {
return Object.values(this.logicalValidityMap).some((isValid) => !isValid.valid);
}, },
inputTimeSystem: { isValid() {
handler(newTimeSystem) { return !this.hasInputValidityError && !this.hasLogicalValidationErrors;
this.setTimeSystem(newTimeSystem);
},
deep: true
} }
}, },
created() { watch: {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300); bounds: {
handler() {
this.setViewFromBounds();
}
}
}, },
mounted() { mounted() {
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem()))); this.setViewFromBounds();
this.setViewFromBounds(this.bounds);
}, },
beforeUnmount() { beforeUnmount() {
this.clearAllValidation(); this.clearAllValidation();
}, },
methods: { methods: {
handleNewBounds(bounds) { setViewFromBounds() {
this.setBounds(bounds); const start = this.timeSystemFormatter.format(this.bounds.start);
this.setViewFromBounds(bounds); const end = this.timeSystemFormatter.format(this.bounds.end);
},
clearAllValidation() { this.formattedBounds = {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput); start,
}, end
clearValidationForInput(input) { };
if (input) {
input.setCustomValidity('');
input.title = '';
}
},
setBounds(bounds) {
this.bounds = bounds;
},
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start).split(' ')[0];
this.formattedBounds.end = this.timeFormatter.format(bounds.end).split(' ')[0];
this.formattedBounds.startTime = this.durationFormatter.format(Math.abs(bounds.start));
this.formattedBounds.endTime = this.durationFormatter.format(Math.abs(bounds.end));
},
setTimeSystem(timeSystem) {
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
this.isUTCBased = timeSystem.isUTCBased;
},
getFormatter(key) {
return this.openmct.telemetry.getValueFormatter({
format: key
}).formatter;
}, },
setBoundsFromView(dismiss) { setBoundsFromView(dismiss) {
if (this.$refs.fixedDeltaInput.checkValidity()) { if (this.$refs.fixedDeltaInput.checkValidity()) {
let start = this.timeFormatter.parse( const start = this.timeSystemFormatter.parse(this.formattedBounds.start);
`${this.formattedBounds.start} ${this.formattedBounds.startTime}` const end = this.timeSystemFormatter.parse(this.formattedBounds.end);
);
let end = this.timeFormatter.parse(
`${this.formattedBounds.end} ${this.formattedBounds.endTime}`
);
this.$emit('update', { start, end }); this.timeContext.setBounds({
start,
end
});
} }
if (dismiss) { if (dismiss) {
@ -215,101 +166,107 @@ export default {
return false; return false;
} }
}, },
handleFormSubmission(shouldDismiss) { clearAllValidation() {
this.validateAllBounds('startDate'); Object.keys(this.inputValidityMap).forEach(this.clearValidation);
this.validateAllBounds('endDate'); },
clearValidation(refName) {
const input = this.getInput(refName);
if (!this.isDisabled) { input.setCustomValidity('');
input.title = '';
},
handleFormSubmission(shouldDismiss) {
this.validateLimit();
this.validateBounds();
this.reportLogicalValidity();
if (this.isValid) {
this.setBoundsFromView(shouldDismiss); this.setBoundsFromView(shouldDismiss);
} }
}, },
validateAllBounds(ref) { validateInput(refName) {
this.isDisabled = false; this.clearAllValidation();
if (!this.areBoundsFormatsValid()) { const validationResult = this.timeSystemFormatter.validate(this.formattedBounds[refName])
this.isDisabled = true;
return false;
}
let validationResult = { valid: true };
const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(
`${this.formattedBounds.start} ${this.formattedBounds.startTime}`
),
end: this.timeFormatter.parse(
`${this.formattedBounds.end} ${this.formattedBounds.endTime}`
)
};
//TODO: Do we need limits here? We have conductor limits disabled right now
// const limit = this.getBoundsLimit();
const limit = false;
if (this.isUTCBased && limit && boundsValues.end - boundsValues.start > limit) {
if (input === currentInput) {
validationResult = {
valid: false,
message: 'Start and end difference exceeds allowable limit'
};
}
} else if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
return this.handleValidationResults(input, validationResult);
});
},
areBoundsFormatsValid() {
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
const formattedDate =
input === this.$refs.startDate
? `${this.formattedBounds.start} ${this.formattedBounds.startTime}`
: `${this.formattedBounds.end} ${this.formattedBounds.endTime}`;
const validationResult = this.timeFormatter.validate(formattedDate)
? { valid: true } ? { valid: true }
: { valid: false, message: 'Invalid date' }; : { valid: false, message: `Invalid Time` };
return this.handleValidationResults(input, validationResult); this.inputValidityMap[refName] = validationResult;
});
}, },
getBoundsLimit() { validateBounds() {
const configuration = this.configuration.menuOptions const bounds = {
.filter((option) => option.timeSystem === this.timeSystem.key) start: this.timeSystemFormatter.parse(this.formattedBounds.start),
.find((option) => option.limit); end: this.timeSystemFormatter.parse(this.formattedBounds.end)
};
const limit = configuration ? configuration.limit : undefined; this.logicalValidityMap.bounds = this.timeContext.validateBounds(bounds);
return limit;
}, },
handleValidationResults(input, validationResult) { validateLimit() {
const bounds = {
start: this.timeSystemFormatter.parse(this.formattedBounds.start),
end: this.timeSystemFormatter.parse(this.formattedBounds.end)
};
const limit = this.configuration?.menuOptions
?.filter((option) => option.timeSystem === this.timeSystemKey)
?.find((option) => option.limit)?.limit;
if (this.isTimeSystemUTCBased && limit && bounds.end - bounds.start > limit) {
this.logicalValidityMap.limit = {
valid: false,
message: `Start and end difference exceeds allowable limit of ${limit}`
};
} else {
this.logicalValidityMap.limit = { valid: true };
}
},
reportValidity(refName) {
const input = this.getInput(refName);
const validationResult = this.inputValidityMap[refName];
if (validationResult.valid !== true) { if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message); input.setCustomValidity(validationResult.message);
input.title = validationResult.message; input.title = validationResult.message;
this.isDisabled = true;
} else { } else {
input.setCustomValidity(''); input.setCustomValidity('');
input.title = ''; input.title = '';
} }
this.$refs.fixedDeltaInput.reportValidity(); this.$refs.fixedDeltaInput.reportValidity();
},
reportLogicalValidity() {
const input = this.getInput();
const boundsValidationResult = this.logicalValidityMap.bounds;
const limitValidationResult = this.logicalValidityMap.limit;
return validationResult.valid; if (boundsValidationResult.valid !== true) {
input.setCustomValidity(boundsValidationResult.message);
input.title = boundsValidationResult.message;
} else if (limitValidationResult.valid !== true) {
input.setCustomValidity(limitValidationResult.message);
input.title = limitValidationResult.message;
} else {
input.setCustomValidity('');
input.title = '';
}
this.$refs.fixedDeltaInput.reportValidity();
}, },
startDateSelected(date) { getInput(refName) {
this.formattedBounds.start = this.timeFormatter.format(date).split(' ')[0]; if (Object.keys(this.inputValidityMap).includes(refName)) {
this.validateAllBounds('startDate'); return this.$refs[refName];
}, }
endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date).split(' ')[0]; return this.$refs.start;
this.validateAllBounds('endDate');
}, },
hide($event) { hide($event) {
if ($event.target.className.indexOf('c-button icon-x') > -1) { if ($event.target.className.indexOf('c-button icon-x') > -1) {
this.$emit('dismiss'); this.$emit('dismiss');
} }
},
dateSelected(date, refName) {
this.formattedBounds[refName] = this.timeSystemFormatter.format(date);
this.validateInput(refName);
} }
} }
}; };

View File

@ -1,6 +1,28 @@
<!--
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> <template>
<form ref="deltaInput"> <form ref="deltaInput">
<div class="c-tc-input-popup__input-grid"> <div class="c-tc-input-popup__input-grid-utc">
<div class="pr-time-label icon-minus pr-time-label-minus-hrs">Hrs</div> <div class="pr-time-label icon-minus pr-time-label-minus-hrs">Hrs</div>
<div class="pr-time-label pr-time-label-minus-mins">Mins</div> <div class="pr-time-label pr-time-label-minus-mins">Mins</div>
<div class="pr-time-label pr-time-label-minus-secs">Secs</div> <div class="pr-time-label pr-time-label-minus-secs">Secs</div>
@ -142,14 +164,17 @@
</template> </template>
<script> <script>
import { nextTick } from 'vue';
export default { export default {
inject: ['timeContext', 'timeSystemDurationFormatter'],
props: { props: {
offsets: { offsets: {
type: Object, type: Object,
required: true required: true
} }
}, },
emits: ['update', 'dismiss'], emits: ['dismiss'],
data() { data() {
return { return {
startInputHrs: '00', startInputHrs: '00',
@ -164,6 +189,7 @@ export default {
watch: { watch: {
offsets: { offsets: {
handler() { handler() {
console.log('REMOVE THIS');
this.setOffsets(); this.setOffsets();
}, },
deep: true deep: true
@ -206,18 +232,23 @@ export default {
this.isDisabled = disabled; this.isDisabled = disabled;
}, },
submit() { submit() {
this.$emit('update', { const formattedStartOffset = [
start: { this.startInputHrs,
hours: this.startInputHrs, this.startInputMins,
minutes: this.startInputMins, this.startInputSecs
seconds: this.startInputSecs ].join(':');
}, const formattedEndOffset = [this.endInputHrs, this.endInputMins, this.endInputSecs].join(':');
end: {
hours: this.endInputHrs, let startOffset = 0 - this.timeSystemDurationFormatter.parse(formattedStartOffset);
minutes: this.endInputMins, let endOffset = this.timeSystemDurationFormatter.parse(formattedEndOffset);
seconds: this.endInputSecs
} const offsets = {
}); start: startOffset,
end: endOffset
};
this.timeContext.setClockOffsets(offsets);
this.$emit('dismiss'); this.$emit('dismiss');
}, },
hide($event) { hide($event) {
@ -234,13 +265,13 @@ export default {
this[ref] = cv.toString().padStart(2, '0'); this[ref] = cv.toString().padStart(2, '0');
this.validate(); this.validate();
}, },
setOffsets() { async setOffsets() {
[this.startInputHrs, this.startInputMins, this.startInputSecs] = [this.startInputHrs, this.startInputMins, this.startInputSecs] =
this.offsets.start.split(':'); this.offsets.start.split(':');
[this.endInputHrs, this.endInputMins, this.endInputSecs] = this.offsets.end.split(':'); [this.endInputHrs, this.endInputMins, this.endInputSecs] = this.offsets.end.split(':');
this.$nextTick(() => {
await nextTick();
this.numberSelect('startInputHrs'); this.numberSelect('startInputHrs');
});
}, },
numberSelect(input) { numberSelect(input) {
if (this.$refs[input] === undefined || this.$refs[input] === null) { if (this.$refs[input] === undefined || this.$refs[input] === null) {

View File

@ -1,13 +1,4 @@
export default { export default {
props: {
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
}
},
methods: { methods: {
loadClocks(menuOptions) { loadClocks(menuOptions) {
let clocks; let clocks;

View File

@ -629,7 +629,6 @@
} }
&-label-end-time{ &-label-end-time{
grid-area: eTime; grid-area: eTime;
} }
&-input-end-date{ &-input-end-date{
grid-area: eDateInput; grid-area: eDateInput;
@ -641,6 +640,14 @@
grid-area: blank; grid-area: blank;
} }
// FIXED TIME MODE non utc
&-input-start{
grid-area: sInput;
}
&-input-end{
grid-area: eInput;
}
//REAL TIME MODE //REAL TIME MODE
&-label-minus-hrs{ &-label-minus-hrs{
grid-area: labelMinusHrs; grid-area: labelMinusHrs;
@ -691,14 +698,20 @@
} }
&--fixed-mode { &--fixed-mode {
.c-tc-input-popup__input-grid { .c-tc-input-popup__input-grid-utc {
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 2fr; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 2fr;
grid-template-areas: grid-template-areas:
"sDate sTime . eDate eTime ." "sDate sTime . eDate eTime ."
"sDateInput sTimeInput arrowIcon eDateInput eTimeInput buttons"; "sDateInput sTimeInput arrowIcon eDateInput eTimeInput buttons";
} }
@include phonePortrait(){
.c-tc-input-popup__input-grid { .c-tc-input-popup__input-grid {
grid-template-columns: 1fr 1fr 1fr 2fr;
grid-template-areas:
"sTime . eTime ."
"sInput arrowIcon eInput buttons";
}
@include phonePortrait(){
.c-tc-input-popup__input-grid-utc {
grid-template-columns: repeat(2, max-content) 1fr; grid-template-columns: repeat(2, max-content) 1fr;
grid-template-areas: grid-template-areas:
"sDate sTime ." "sDate sTime ."
@ -708,19 +721,29 @@
padding: 2px; padding: 2px;
overflow: hidden; overflow: hidden;
} }
.c-tc-input-popup__input-grid {
grid-template-columns: repeat(2, max-content) 1fr;
grid-template-areas:
"sTime ."
"sInput ."
"eTime ."
"eInput buttons";
padding: 2px;
overflow: hidden;
}
} }
} }
&--realtime-mode { &--realtime-mode {
.c-tc-input-popup__input-grid { .c-tc-input-popup__input-grid, .c-tc-input-popup__input-grid-utc {
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr;
grid-template-areas: grid-template-areas:
"labelMinusHrs labelMinusMins labelMinusSecs . labelPlusHrs labelPlusMins labelPlusSecs ." "labelMinusHrs labelMinusMins labelMinusSecs . labelPlusHrs labelPlusMins labelPlusSecs ."
"inputMinusHrs inputMinusMins inputMinusSecs arrowIcon inputPlusHrs inputPlusMins inputPlusSecs buttons"; "inputMinusHrs inputMinusMins inputMinusSecs arrowIcon inputPlusHrs inputPlusMins inputPlusSecs buttons";
} }
@include phonePortrait(){ @include phonePortrait(){
.c-tc-input-popup__input-grid { .c-tc-input-popup__input-grid, .c-tc-input-popup__input-grid-utc {
grid-template-columns: repeat(3, max-content) 1fr; grid-template-columns: repeat(3, max-content) 1fr;
grid-template-areas: grid-template-areas:
"labelMinusHrs labelMinusMins labelMinusSecs ." "labelMinusHrs labelMinusMins labelMinusSecs ."
@ -733,7 +756,7 @@
} }
} }
&__input-grid { &__input-grid, &__input-grid-utc {
display: grid; display: grid;
grid-row-gap: $interiorMargin; grid-row-gap: $interiorMargin;
grid-column-gap: $interiorMarginSm; grid-column-gap: $interiorMarginSm;

View File

@ -25,7 +25,7 @@
<button <button
v-if="selectedClock" v-if="selectedClock"
class="c-icon-button c-button--menu js-clock-button" class="c-icon-button c-button--menu js-clock-button"
:class="[buttonCssClass, selectedClock.cssClass]" :class="selectedClock.cssClass"
aria-label="Independent Time Conductor Clock Menu" aria-label="Independent Time Conductor Clock Menu"
@click.prevent.stop="showClocksMenu" @click.prevent.stop="showClocksMenu"
> >
@ -36,20 +36,9 @@
</template> </template>
<script> <script>
import { TIME_CONTEXT_EVENTS } from '../../../api/time/constants.js';
import toggleMixin from '../../../ui/mixins/toggle-mixin.js';
import clockMixin from '../clock-mixin.js';
export default { export default {
mixins: [toggleMixin, clockMixin], inject: ['openmct', 'clock', 'getAllClockMetadata', 'getClockMetadata'],
inject: ['openmct'],
props: { props: {
clock: {
type: String,
default() {
return undefined;
}
},
enabled: { enabled: {
type: Boolean, type: Boolean,
default() { default() {
@ -57,33 +46,20 @@ export default {
} }
} }
}, },
emits: ['independent-clock-updated'], computed: {
data() { selectedClock() {
const activeClock = this.getActiveClock(); return this.getClockMetadata(this.clock);
}
return {
selectedClock: activeClock ? this.getClockMetadata(activeClock) : undefined,
clocks: []
};
}, },
watch: { watch: {
clock(newClock, oldClock) {
this.setViewFromClock(newClock);
},
enabled(newValue, oldValue) { enabled(newValue, oldValue) {
if (newValue !== undefined && newValue !== oldValue && newValue === true) { if (newValue !== undefined && newValue !== oldValue && newValue === true) {
this.setViewFromClock(this.clock); this.setViewFromClock(this.clock);
} }
} }
}, },
beforeUnmount() { mounted() {
this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock); this.clocks = this.getAllClockMetadata();
},
mounted: function () {
this.loadClocks();
this.setViewFromClock(this.clock);
this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
}, },
methods: { methods: {
showClocksMenu() { showClocksMenu() {
@ -95,27 +71,11 @@ export default {
menuClass: 'c-conductor__clock-menu c-super-menu--sm', menuClass: 'c-conductor__clock-menu c-super-menu--sm',
placement: this.openmct.menus.menuPlacement.BOTTOM_RIGHT placement: this.openmct.menus.menuPlacement.BOTTOM_RIGHT
}; };
this.openmct.menus.showSuperMenu(x, y, this.clocks, menuOptions); this.openmct.menus.showSuperMenu(x, y, this.clocks, menuOptions);
}, },
getMenuOptions() {
let currentGlobalClock = this.getActiveClock();
//Create copy of active clock so the time API does not get reactified.
currentGlobalClock = Object.assign(
{},
{
name: currentGlobalClock.name,
clock: currentGlobalClock.key,
timeSystem: this.openmct.time.getTimeSystem().key
}
);
return [currentGlobalClock];
},
setClock(clockKey) { setClock(clockKey) {
this.setViewFromClock(clockKey); this.setViewFromClock(clockKey);
this.$emit('independent-clock-updated', clockKey);
}, },
setViewFromClock(clockOrKey) { setViewFromClock(clockOrKey) {
let clock = clockOrKey; let clock = clockOrKey;

View File

@ -23,8 +23,8 @@
<div ref="modeMenuButton" class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"> <div ref="modeMenuButton" class="c-ctrl-wrapper c-ctrl-wrapper--menus-up">
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left"> <div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button <button
class="c-icon-button c-button--menu js-mode-button" class="c-button--menu js-mode-button c-icon-button"
:class="[buttonCssClass, selectedMode.cssClass]" :class="selectedMode.cssClass"
aria-label="Independent Time Conductor Mode Menu" aria-label="Independent Time Conductor Mode Menu"
@click.prevent.stop="showModesMenu" @click.prevent.stop="showModesMenu"
> >
@ -35,19 +35,10 @@
</template> </template>
<script> <script>
import toggleMixin from '../../../ui/mixins/toggle-mixin.js';
import modeMixin from '../mode-mixin.js';
export default { export default {
mixins: [toggleMixin, modeMixin], inject: ['openmct', 'timeMode', 'getAllModeMetadata', 'getModeMetadata'],
inject: ['openmct'],
props: { props: {
mode: {
type: String,
default() {
return undefined;
}
},
enabled: { enabled: {
type: Boolean, type: Boolean,
default() { default() {
@ -55,27 +46,25 @@ export default {
} }
} }
}, },
emits: ['independent-mode-updated'], data() {
data: function () {
return { return {
selectedMode: this.getModeMetadata(this.mode), selectedMode: this.getModeMetadata(this.timeMode)
modes: []
}; };
}, },
watch: { watch: {
mode: { timeMode: {
handler(newMode) { handler() {
this.setViewFromMode(newMode); this.setView();
} }
}, },
enabled(newValue, oldValue) { enabled(newValue, oldValue) {
if (newValue !== undefined && newValue !== oldValue && newValue === true) { if (newValue !== undefined && newValue !== oldValue && newValue === true) {
this.setViewFromMode(this.mode); this.setView();
} }
} }
}, },
mounted: function () { mounted: function () {
this.loadModes(); this.modes = this.getAllModeMetadata();
}, },
methods: { methods: {
showModesMenu() { showModesMenu() {
@ -89,13 +78,8 @@ export default {
}; };
this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions); this.openmct.menus.showSuperMenu(x, y, this.modes, menuOptions);
}, },
setViewFromMode(mode) { setView() {
this.selectedMode = this.getModeMetadata(mode); this.selectedMode = this.getModeMetadata(this.timeMode);
},
setMode(mode) {
this.setViewFromMode(mode);
this.$emit('independent-mode-updated', mode);
} }
} }
}; };

View File

@ -24,7 +24,7 @@
ref="timeConductorOptionsHolder" ref="timeConductorOptionsHolder"
class="c-compact-tc" class="c-compact-tc"
:class="[ :class="[
isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode', isFixedTimeMode ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode',
{ 'is-expanded': independentTCEnabled } { 'is-expanded': independentTCEnabled }
]" ]"
aria-label="Independent Time Conductor Panel" aria-label="Independent Time Conductor Panel"
@ -42,7 +42,6 @@
<ConductorInputsFixed <ConductorInputsFixed
v-if="showFixedInputs" v-if="showFixedInputs"
class="c-compact-tc__bounds--fixed" class="c-compact-tc__bounds--fixed"
:object-path="objectPath"
:read-only="true" :read-only="true"
:compact="true" :compact="true"
/> />
@ -50,45 +49,38 @@
<ConductorInputsRealtime <ConductorInputsRealtime
v-if="showRealtimeInputs" v-if="showRealtimeInputs"
class="c-compact-tc__bounds--real-time" class="c-compact-tc__bounds--real-time"
:object-path="objectPath"
:read-only="true" :read-only="true"
:compact="true" :compact="true"
/> />
<div
v-if="independentTCEnabled"
role="button"
class="c-not-button c-not-button--compact c-compact-tc__gear icon-gear"
aria-label="Independent Time Conductor Settings"
></div>
<ConductorPopUp <ConductorPopUp
v-if="showConductorPopup" v-if="showConductorPopup"
ref="conductorPopup" ref="conductorPopup"
:object-path="objectPath"
:is-independent="true" :is-independent="true"
:time-options="timeOptions"
:is-fixed="isFixed"
:bottom="true" :bottom="true"
:position-x="positionX" :position-x="positionX"
:position-y="positionY" :position-y="positionY"
@popup-loaded="initializePopup" @popup-loaded="initializePopup"
@independent-mode-updated="saveMode"
@independent-clock-updated="saveClock"
@fixed-bounds-updated="saveFixedBounds"
@clock-offsets-updated="saveClockOffsets"
@dismiss="clearPopup" @dismiss="clearPopup"
/> />
</div> </div>
</template> </template>
<script> <script>
import { inject, provide, toRaw } from 'vue';
import ConductorModeIcon from '@/plugins/timeConductor/ConductorModeIcon.vue'; import ConductorModeIcon from '@/plugins/timeConductor/ConductorModeIcon.vue';
import { FIXED_MODE_KEY, TIME_CONTEXT_EVENTS } from '../../../api/time/constants.js';
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue'; import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import ConductorInputsFixed from '../ConductorInputsFixed.vue'; import ConductorInputsFixed from '../ConductorInputsFixed.vue';
import ConductorInputsRealtime from '../ConductorInputsRealtime.vue'; import ConductorInputsRealtime from '../ConductorInputsRealtime.vue';
import ConductorPopUp from '../ConductorPopUp.vue'; import ConductorPopUp from '../ConductorPopUp.vue';
import { useClock } from '../useClock.js';
import { useClockOffsets } from '../useClockOffsets.js';
import { useTimeBounds } from '../useTimeBounds.js';
import { useTimeContext } from '../useTimeContext.js';
import { useTimeMode } from '../useTimeMode.js';
import { useTimeSystem } from '../useTimeSystem.js';
import independentTimeConductorPopUpManager from './independentTimeConductorPopUpManager.js'; import independentTimeConductorPopUpManager from './independentTimeConductorPopUpManager.js';
export default { export default {
@ -100,13 +92,7 @@ export default {
ToggleSwitch ToggleSwitch
}, },
mixins: [independentTimeConductorPopUpManager], mixins: [independentTimeConductorPopUpManager],
inject: { inject: ['openmct'],
openmct: 'openmct',
configuration: {
from: 'configuration',
default: undefined
}
},
props: { props: {
domainObject: { domainObject: {
type: Object, type: Object,
@ -117,210 +103,257 @@ export default {
required: true required: true
} }
}, },
emits: ['updated'], setup(props) {
data() { const openmct = inject('openmct');
const fixedOffsets = this.openmct.time.getBounds(); const { timeContext } = useTimeContext(openmct, () => props.objectPath);
const clockOffsets = this.openmct.time.getClockOffsets();
const clock = this.openmct.time.getClock().key;
const mode = this.openmct.time.getMode();
const timeOptions = this.domainObject.configuration.timeOptions ?? {
clockOffsets,
fixedOffsets
};
timeOptions.clock = timeOptions.clock ?? clock; const {
timeOptions.mode = timeOptions.mode ?? mode; timeSystemKey,
timeSystemFormatter,
timeSystemDurationFormatter,
isTimeSystemUTCBased
} = useTimeSystem(openmct, timeContext);
const { timeMode, isFixedTimeMode, isRealTimeMode, getAllModeMetadata, getModeMetadata } =
useTimeMode(openmct, timeContext);
const { bounds, isTick } = useTimeBounds(openmct, timeContext);
const { clock, getAllClockMetadata, getClockMetadata } = useClock(openmct, timeContext);
const { offsets } = useClockOffsets(openmct, timeContext);
// check for older configurations that stored a key provide('timeContext', timeContext);
if (timeOptions.mode.key) { provide('timeSystemKey', timeSystemKey);
timeOptions.mode = timeOptions.mode.key; provide('timeSystemFormatter', timeSystemFormatter);
} provide('timeSystemDurationFormatter', timeSystemDurationFormatter);
provide('isTimeSystemUTCBased', isTimeSystemUTCBased);
const isFixed = timeOptions.mode === FIXED_MODE_KEY; provide('timeMode', timeMode);
provide('isFixedTimeMode', isFixedTimeMode);
provide('isRealTimeMode', isRealTimeMode);
provide('getAllModeMetadata', getAllModeMetadata);
provide('getModeMetadata', getModeMetadata);
provide('bounds', bounds);
provide('isTick', isTick);
provide('offsets', offsets);
provide('clock', clock);
provide('getAllClockMetadata', getAllClockMetadata);
provide('getClockMetadata', getClockMetadata);
return { return {
timeOptions, timeContext,
isFixed, timeMode,
independentTCEnabled: this.domainObject.configuration.useIndependentTime === true, clock,
viewBounds: { timeSystemFormatter,
start: fixedOffsets.start, isFixedTimeMode,
end: fixedOffsets.end isRealTimeMode,
} bounds,
isTick,
offsets
};
},
data() {
return {
keyString: this.openmct.objects.makeKeyString(this.domainObject.identifier),
independentTCEnabled: this.domainObject.configuration.useIndependentTime === true
}; };
}, },
computed: { computed: {
myKeyString() {
const identifier = this.domainObject.identifier;
return this.openmct.objects.makeKeyString(identifier);
},
do() {
console.log(this.objectPath[0]);
return this.objectPath[0];
},
// itcEnabled() {
// console.log(`itcEnabled: ${this.domainObject.configuration.useIndependentTime === true}`);
// return this.domainObject.configuration.useIndependentTime === true;
// },
configuration() {
console.log('why does this not fire when watch domainObject fires?');
return this.domainObject.configuration && {};
},
toggleTitle() { toggleTitle() {
return `${this.independentTCEnabled ? 'Disable' : 'Enable'} Independent Time Conductor`; return `${this.independentTCEnabled ? 'Disable' : 'Enable'} Independent Time Conductor`;
}, },
showFixedInputs() { showFixedInputs() {
return this.isFixed && this.independentTCEnabled; return this.isFixedTimeMode && this.independentTCEnabled;
}, },
showRealtimeInputs() { showRealtimeInputs() {
return !this.isFixed && this.independentTCEnabled; return this.isRealTimeMode && this.independentTCEnabled;
} }
}, },
watch: { watch: {
domainObject: { myKeyString() {
handler(domainObject) { console.log(`object changed`);
const key = this.openmct.objects.makeKeyString(domainObject.identifier); },
if (key !== this.keyString) { independentTCEnabled() {
//domain object has changed this.handleIndependentTimeConductorChange();
this.destroyIndependentTime(); },
timeContext() {
const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.independentTCEnabled = domainObject.configuration.useIndependentTime === true; if (keyString !== this.keyString) {
this.timeOptions = domainObject.configuration.timeOptions ?? { //domain object in object view has changed (via tree navigation)
clockOffsets: this.openmct.time.getClockOffsets(), this.unregisterIndependentTimeContext?.();
fixedOffsets: this.openmct.time.getBounds() this.keyString = keyString;
};
// these may not be set due to older configurations this.independentTCEnabled = this.domainObject.configuration.useIndependentTime === true;
this.timeOptions.clock = this.timeOptions.clock ?? this.openmct.time.getClock().key;
this.timeOptions.mode = this.timeOptions.mode ?? this.openmct.time.getMode();
// check for older configurations that stored a key this.setTimeOptions();
if (this.timeOptions.mode.key) {
this.timeOptions.mode = this.timeOptions.mode.key;
}
this.isFixed = this.timeOptions.mode === FIXED_MODE_KEY;
this.initialize(); this.initialize();
} }
}, },
deep: true independentTCEnabled() {
this.handleIndependentTimeConductorChange();
}, },
objectPath: { clock() {
handler(newPath, oldPath) { if (this.independentTCEnabled) {
//domain object or view has probably changed this.saveClock();
this.setTimeContext(); }
}, },
deep: true timeMode() {
if (this.independentTCEnabled) {
this.saveMode();
}
},
clockOffsets() {
if (this.independentTCEnabled) {
this.saveClockOffsets();
}
},
bounds() {
if (this.independentTCEnabled && this.isTick === false) {
this.saveFixedBounds();
}
} }
}, },
created() { created() {
// this.initialize();
},
mounted() {
this.setTimeOptions();
this.initialize(); this.initialize();
}, },
beforeUnmount() { beforeUnmount() {
this.stopFollowingTimeContext(); this.unregisterIndependentTimeContext?.();
this.destroyIndependentTime();
}, },
methods: { methods: {
initialize() { initialize() {
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.setTimeContext();
if (this.independentTCEnabled) { if (this.independentTCEnabled) {
this.registerIndependentTimeOffsets(); this.registerIndependentTimeContext();
}
},
handleIndependentTimeConductorChange() {
if (this.independentTCEnabled) {
this.registerIndependentTimeContext();
} else {
this.clearPopup();
this.unregisterIndependentTimeContext?.();
} }
}, },
toggleIndependentTC() { toggleIndependentTC() {
this.independentTCEnabled = !this.independentTCEnabled; this.independentTCEnabled = !this.independentTCEnabled;
if (this.independentTCEnabled) {
this.registerIndependentTimeOffsets();
} else {
this.clearPopup();
this.destroyIndependentTime();
}
this.openmct.objects.mutate( this.openmct.objects.mutate(
this.domainObject, this.domainObject,
'configuration.useIndependentTime', 'configuration.useIndependentTime',
this.independentTCEnabled this.independentTCEnabled
); );
}, },
setTimeContext() { setTimeOptions() {
if (this.timeContext) { this.timeOptions = toRaw(this.domainObject.configuration.timeOptions);
this.stopFollowingTimeContext(); if (!this.timeOptions) {
this.timeOptions = {
clockOffsets: this.offsets,
fixedOffsets: this.bounds
};
} }
this.timeContext = this.openmct.time.getContextForView(this.objectPath); if (!this.timeOptions.clock) {
this.timeContext.on(TIME_CONTEXT_EVENTS.clockChanged, this.setTimeOptionsClock); // can remove openmct.time.getClock() if timeContexts have clock in fixed time
this.timeContext.on(TIME_CONTEXT_EVENTS.modeChanged, this.setTimeOptionsMode); this.timeOptions.clock = this.clock?.key ?? this.openmct.time.getClock().key;
}, }
stopFollowingTimeContext() {
this.timeContext.off(TIME_CONTEXT_EVENTS.clockChanged, this.setTimeOptionsClock);
this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setTimeOptionsMode);
},
setTimeOptionsClock(clock) {
this.setTimeOptionsOffsets();
this.timeOptions.clock = clock.key;
},
setTimeOptionsMode(mode) {
this.setTimeOptionsOffsets();
this.timeOptions.mode = mode;
},
setTimeOptionsOffsets() {
this.timeOptions.clockOffsets =
this.timeOptions.clockOffsets ?? this.timeContext.getClockOffsets();
this.timeOptions.fixedOffsets = this.timeOptions.fixedOffsets ?? this.timeContext.getBounds();
},
saveFixedBounds(bounds) {
const newOptions = this.updateTimeOptionProperty({
fixedOffsets: bounds
});
this.updateTimeOptions(newOptions);
},
saveClockOffsets(offsets) {
const newOptions = this.updateTimeOptionProperty({
clockOffsets: offsets
});
this.updateTimeOptions(newOptions); if (!this.timeOptions.mode) {
}, this.timeOptions.mode = this.timeMode;
saveMode(mode) { }
this.isFixed = mode === FIXED_MODE_KEY;
const newOptions = this.updateTimeOptionProperty({
mode: mode
});
this.updateTimeOptions(newOptions); // check for older configurations that stored a key
if (this.timeOptions.mode.key) {
this.timeOptions.mode = this.timeOptions.mode.key;
}
}, },
saveClock(clock) { saveFixedBounds() {
const newOptions = this.updateTimeOptionProperty({ this.timeOptions.fixedOffsets = this.bounds;
clock this.updateTimeOptions();
});
this.updateTimeOptions(newOptions);
}, },
updateTimeOptions(options) { saveClockOffsets() {
this.timeOptions = options; this.timeOptions.clockOffsets = this.offsets;
this.updateTimeOptions();
this.registerIndependentTimeOffsets(); },
this.$emit('updated', this.timeOptions); // no longer use this, but may be used elsewhere saveMode() {
this.timeOptions.mode = this.timeMode;
this.updateTimeOptions();
},
saveClock() {
this.timeOptions.clock = this.clock?.key;
this.updateTimeOptions();
},
updateTimeOptions() {
this.registerIndependentTimeContext();
this.openmct.objects.mutate(this.domainObject, 'configuration.timeOptions', this.timeOptions); this.openmct.objects.mutate(this.domainObject, 'configuration.timeOptions', this.timeOptions);
}, },
registerIndependentTimeOffsets() { registerIndependentTimeContext() {
const timeContext = this.openmct.time.getIndependentContext(this.keyString); const bounds = this.timeOptions.fixedOffsets ?? this.bounds;
let offsets; const offsets = this.timeOptions.clockOffsets ?? this.offsets;
const clockKey = this.timeOptions.clock || this.clock.key;
if (this.isFixed) { const independentTimeContextBoundsOrOffsets = this.isFixedTimeMode ? bounds : offsets;
offsets = this.timeOptions.fixedOffsets ?? this.timeContext.getBounds(); const independentTimeContextClockKey = this.isFixedTimeMode ? undefined : clockKey;
} else {
offsets = this.timeOptions.clockOffsets ?? this.openmct.time.getClockOffsets();
}
if (!timeContext.hasOwnContext()) { const independentTimeContext = this.openmct.time.getIndependentContext(this.keyString);
this.unregisterIndependentTime = this.openmct.time.addIndependentContext(
if (!independentTimeContext.hasOwnContext()) {
this.unregisterIndependentTimeContext = this.openmct.time.addIndependentContext(
this.keyString, this.keyString,
offsets, independentTimeContextBoundsOrOffsets,
this.isFixed ? undefined : this.timeOptions.clock independentTimeContextClockKey
); );
} else { } else {
if (!this.isFixed) { // if (this.isRealTimeMode) {
timeContext.setClock(this.timeOptions.clock); // independentTimeContext.setClock(this.timeOptions.clock);
// }
// independentTimeContext.setMode(this.timeOptions.mode, independentTimeContextBoundsOrOffsets);
}
},
registerIndependentTimeOffsets() {
// const timeContext = this.openmct.time.getIndependentContext(this.keyString);
const clockKey = this.timeOptions.clock || this.clock.key;
let offsets;
if (this.isFixedTimeMode) {
offsets = this.timeOptions.fixedOffsets ?? this.bounds;
} else {
offsets = this.timeOptions.clockOffsets ?? this.offsets;
} }
timeContext.setMode(this.timeOptions.mode, offsets); if (!this.timeContext.hasOwnContext()) {
this.unregisterIndependentTimeContext = this.openmct.time.addIndependentContext(
this.keyString,
offsets,
this.isFixedTimeMode ? undefined : clockKey
);
} else {
console.log('removed code');
// if (this.isRealTimeMode) {
// this.timeContext.setClock(this.timeOptions.clock);
// }
// this.timeContext.setMode(this.timeOptions.mode, offsets);
} }
},
destroyIndependentTime() {
if (this.unregisterIndependentTime) {
this.unregisterIndependentTime();
}
},
updateTimeOptionProperty(option) {
return Object.assign({}, this.timeOptions, option);
} }
} }
}; };

View File

@ -1,15 +1,6 @@
import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '../../api/time/constants.js'; import { FIXED_MODE_KEY, REALTIME_MODE_KEY } from '../../api/time/constants.js';
export default { export default {
props: {
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
}
},
methods: { methods: {
loadModes() { loadModes() {
this.modes = [FIXED_MODE_KEY, REALTIME_MODE_KEY].map(this.getModeMetadata); this.modes = [FIXED_MODE_KEY, REALTIME_MODE_KEY].map(this.getModeMetadata);

View File

@ -140,17 +140,13 @@ export default function (config) {
} }
} }
openmct.time.setMode(defaultMode, defaultClock ? clockOffsets : defaultBounds);
openmct.time.setTimeSystem(defaults.timeSystem, defaultBounds);
//We are going to set the clockOffsets in fixed time mode since the conductor components down the line need these //We are going to set the clockOffsets in fixed time mode since the conductor components down the line need these
if (clockOffsets && defaultMode === FIXED_MODE_KEY) { if (clockOffsets && defaultMode === FIXED_MODE_KEY) {
openmct.time.setClockOffsets(clockOffsets); openmct.time.setClockOffsets(clockOffsets);
} }
//We are going to set the fixed time bounds in realtime time mode since the conductor components down the line need these
if (defaultBounds && defaultMode === REALTIME_MODE_KEY) { openmct.time.setMode(defaultMode, defaultClock ? clockOffsets : defaultBounds);
openmct.time.setBounds(clockOffsets); openmct.time.setTimeSystem(defaults.timeSystem, defaultBounds);
}
openmct.on('start', function () { openmct.on('start', function () {
mountComponent(openmct, config); mountComponent(openmct, config);

View File

@ -0,0 +1,151 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web 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 { onBeforeUnmount, shallowRef, watch } from 'vue';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
/**
* Provides reactive `clock` which is reactive to a time context,
* as well as a function to observe and update the component's clock,
* which automatically stops observing when the component is unmounted.
*
* @param {OpenMCT} [openmct] the Open MCT API
* @param {Array} objectPath The view's objectPath
* @returns {{
* clock: import('vue').Ref<string>,
* getAllClockMetadata: () => Object,
* getClockMetadata: () => Object
* }}
*/
export function useClock(openmct, timeContext) {
let stopObservingClock;
const clock = shallowRef(timeContext.value.getClock());
onBeforeUnmount(() => stopObservingClock?.());
watch(
timeContext,
(newContext, oldContext) => {
oldContext?.off(TIME_CONTEXT_EVENTS.clockChanged, updateClock);
observeClock();
},
{ immediate: true }
);
function observeClock() {
timeContext.value.on(TIME_CONTEXT_EVENTS.clockChanged, updateClock);
stopObservingClock = () => timeContext.value.off(TIME_CONTEXT_EVENTS.clockChanged, updateClock);
}
function getAllClockMetadata(menuOptions) {
const clocks = menuOptions
? menuOptions
.map((menuOption) => menuOption.clock)
.filter((key, index, array) => key !== undefined && array.indexOf(key) === index)
.map((clockKey) => openmct.time.getAllClocks().find((_clock) => _clock.key === clockKey))
: openmct.time.getAllClocks();
const clockMetadata = clocks.map(getClockMetadata);
return clockMetadata;
}
function getClockMetadata(_clock) {
if (_clock === undefined) {
return;
}
const clockMetadata = {
key: _clock.key,
name: _clock.name,
description: 'Uses the system clock as the current time basis. ' + _clock.description,
cssClass: _clock.cssClass || 'icon-clock',
onItemClicked: () => setClock(_clock.key)
};
return clockMetadata;
}
function setClock(key) {
timeContext.value.setClock(key);
}
function updateClock(_clock) {
clock.value = _clock;
}
/**
* TODO: bring this back. we lost this in the last refactor.
* changing clock requires a timesystem check.
*
function setClockWithOptions() {
const option = {
clockKey
};
let configuration = this.getMatchingConfig({
clock: clockKey,
timeSystem: this.openmct.time.getTimeSystem().key
});
if (configuration === undefined) {
configuration = this.getMatchingConfig({
clock: clockKey
});
option.timeSystem = configuration.timeSystem;
option.bounds = configuration.bounds;
// this.openmct.time.setTimeSystem(configuration.timeSystem, configuration.bounds);
}
const offsets = this.openmct.time.getClockOffsets() ?? configuration.clockOffsets;
option.offsets = offsets;
}
function getMatchingConfig(options) {
const matchers = {
clock(config) {
return options.clock === config.clock;
},
timeSystem(config) {
return options.timeSystem === config.timeSystem;
}
};
function configMatches(config) {
return Object.keys(options).reduce((match, option) => {
return match && matchers[option](config);
}, true);
}
return this.configuration.menuOptions.filter(configMatches)[0];
}
*/
return {
clock,
getAllClockMetadata,
getClockMetadata
};
}

View File

@ -0,0 +1,68 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web 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 { onBeforeUnmount, shallowRef, watch } from 'vue';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
/**
* Provides reactive `offsets`,
* as well as a function to observe and update offsets changes,
* which automatically stops observing when the component is unmounted.
*
* @param {OpenMCT} [openmct] the Open MCT API
* @param {TimeContext} [timeContext] the time context to use for time API clock offsets events
* @returns {{
* observeClockOffsets: () => void,
* offsets: import('vue').Ref<object>,
* }}
*/
export function useClockOffsets(openmct, timeContext) {
let stopObservingClockOffsets;
const offsets = shallowRef(timeContext.value.getClockOffsets());
onBeforeUnmount(() => stopObservingClockOffsets?.());
watch(
timeContext,
(newContext, oldContext) => {
oldContext?.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, updateClockOffsets);
observeClockOffsets();
},
{ immediate: true }
);
function observeClockOffsets() {
timeContext.value.on(TIME_CONTEXT_EVENTS.clockOffsetsChanged, updateClockOffsets);
stopObservingClockOffsets = () =>
timeContext.value.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, updateClockOffsets);
}
function updateClockOffsets(_offsets) {
offsets.value = _offsets;
}
return {
offsets
};
}

View File

@ -0,0 +1,74 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web 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 { onBeforeUnmount, ref, shallowRef, watch } from 'vue';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
import throttle from '../../utils/throttle.js';
const THROTTLE_RATE = 300;
/**
* Provides reactive `bounds`,
* as well as a function to observe and update bounds changes,
* which automatically stops observing when the component is unmounted.
*
* @param {OpenMCT} [openmct] the Open MCT API
* @param {Array} objectPath The view's objectPath
* @returns {{
* bounds: import('vue').Ref<object>,
* isTick: import('vue').Ref<boolean>
* }}
*/
export function useTimeBounds(openmct, timeContext) {
let stopObservingTimeBounds;
const bounds = shallowRef(timeContext.value.getBounds());
const isTick = ref(false);
onBeforeUnmount(() => stopObservingTimeBounds?.());
watch(
timeContext,
(newContext, oldContext) => {
oldContext?.off(TIME_CONTEXT_EVENTS.boundsChanged, throttle(updateTimeBounds, THROTTLE_RATE));
observeTimeBounds();
},
{ immediate: true }
);
function observeTimeBounds() {
timeContext.value.on(TIME_CONTEXT_EVENTS.boundsChanged, throttle(updateTimeBounds, THROTTLE_RATE));
stopObservingTimeBounds = () =>
timeContext.value.off(TIME_CONTEXT_EVENTS.boundsChanged, throttle(updateTimeBounds, THROTTLE_RATE));
}
function updateTimeBounds(_timeBounds, _isTick) {
bounds.value = _timeBounds;
isTick.value = _isTick;
}
return {
isTick,
bounds
};
}

View File

@ -0,0 +1,53 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web 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 { shallowRef, toValue, watchEffect } from 'vue';
/**
* @typedef {import('@/api/time/TimeContext.js').default} TimeContext
* @typedef {import('@/api/time/GlobalTimeContext.js').default} GlobalTimeContext
*/
/**
* Provides the reactive TimeContext
* for the view's objectPath,
* or the GlobalTimeContext if objectPath is undefined.
*
* @param {OpenMCT} openmct the Open MCT API
* @param {Array} objectPath The view's objectPath
* @returns {{
* timeContext: TimeContext | GlobalTimeContext
* }}
*/
export function useTimeContext(openmct, objectPath) {
const timeContext = shallowRef(null);
watchEffect(() => getTimeContext());
function getTimeContext() {
const path = toValue(objectPath);
timeContext.value = path !== undefined ? openmct.time.getContextForView(path) : openmct.time;
}
return { timeContext };
}

View File

@ -0,0 +1,107 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web 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 { computed, onBeforeUnmount, ref, watch } from 'vue';
import {
FIXED_MODE_KEY,
REALTIME_MODE_KEY,
TIME_CONTEXT_EVENTS
} from '../../api/time/constants.js';
/**
* Provides reactive `timeMode` which is reactive to a time context,
* as well as a function to observe and update the component's time mode,
* which automatically stops observing when the component is unmounted.
*
* @param {OpenMCT} openmct the Open MCT API
* @param {Array} objectPath The view's objectPath
* @returns {{
* timeMode: import('vue').Ref<string>,
* isFixedTimeMode: import('vue').Ref<boolean>,
* isRealTimeMode: import('vue').Ref<boolean>
* }}
*/
export function useTimeMode(openmct, timeContext) {
let stopObservingTimeMode;
const timeMode = ref(timeContext.value.getMode());
const isFixedTimeMode = computed(() => timeMode.value === FIXED_MODE_KEY);
const isRealTimeMode = computed(() => timeMode.value === REALTIME_MODE_KEY);
onBeforeUnmount(() => stopObservingTimeMode?.());
watch(
timeContext,
(newContext, oldContext) => {
oldContext?.off(TIME_CONTEXT_EVENTS.modeChanged, updateTimeMode);
observeTimeMode();
},
{ immediate: true }
);
function observeTimeMode() {
timeContext.value.on(TIME_CONTEXT_EVENTS.modeChanged, updateTimeMode);
stopObservingTimeMode = () => timeContext.value.off(TIME_CONTEXT_EVENTS.modeChanged, updateTimeMode);
}
function getAllModeMetadata() {
return [FIXED_MODE_KEY, REALTIME_MODE_KEY].map(getModeMetadata);
}
function getModeMetadata(key) {
const fixedModeMetadata = {
key: FIXED_MODE_KEY,
name: 'Fixed Timespan',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular',
onItemClicked: () => setTimeMode(key)
};
const realTimeModeMetadata = {
key: REALTIME_MODE_KEY,
name: 'Real-Time',
description:
'Monitor streaming data in real-time. The Time Conductor and displays will automatically advance themselves based on the active clock.',
cssClass: 'icon-clock',
onItemClicked: () => setTimeMode(key)
};
return key === FIXED_MODE_KEY ? fixedModeMetadata : realTimeModeMetadata;
}
function setTimeMode(_timeMode) {
timeContext.value.setMode(_timeMode);
}
function updateTimeMode(_timeMode) {
timeMode.value = _timeMode;
}
return {
timeMode,
getAllModeMetadata,
getModeMetadata,
isFixedTimeMode,
isRealTimeMode
};
}

View File

@ -0,0 +1,96 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web 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 { onBeforeUnmount, ref, watch } from 'vue';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
const DEFAULT_DURATION_FORMATTER = 'duration';
/**
* TODO: could probably use a shallowRef for the timeSystem... and all the other components as well.
*
* Provides a reactive destructuring of the component's current time system,
* as well as a function to observe and update the component's time system,
* which automatically stops observing when the component is unmounted.
*
* @param {OpenMCT} openmct the Open MCT API
* @param {Array} objectPath The view's objectPath
* @returns {{
* timeSystemKey: import('vue').Ref<string>,
* timeSystemFormatter: import('vue').Ref<() => void>,
* timeSystemDurationFormatter: import('vue').Ref<() => void>,
* isTimeSystemUTCBased: import('vue').Ref<boolean>
* }}
*/
export function useTimeSystem(openmct, timeContext) {
let stopObservingTimeSystem;
const initialTimeSystem = timeContext.value.getTimeSystem();
const timeSystemKey = ref(initialTimeSystem.key);
const timeSystemFormatter = ref(getFormatter(openmct, initialTimeSystem.timeFormat));
const timeSystemDurationFormatter = ref(
getFormatter(openmct, initialTimeSystem.durationFormat || DEFAULT_DURATION_FORMATTER)
);
const isTimeSystemUTCBased = ref(initialTimeSystem.isUTCBased);
onBeforeUnmount(() => stopObservingTimeSystem?.());
watch(
timeContext,
(newContext, oldContext) => {
oldContext?.off(TIME_CONTEXT_EVENTS.timeSystemChanged, updateTimeSystem);
observeTimeSystem();
},
{ immediate: true }
);
function observeTimeSystem() {
timeContext.value.on(TIME_CONTEXT_EVENTS.timeSystemChanged, updateTimeSystem);
stopObservingTimeSystem = () =>
timeContext.value.off(TIME_CONTEXT_EVENTS.timeSystemChanged, updateTimeSystem);
}
function updateTimeSystem(timeSystem) {
timeSystemKey.value = timeSystem.key;
timeSystemFormatter.value = getFormatter(openmct, timeSystem.timeFormat);
timeSystemDurationFormatter.value = getFormatter(
openmct,
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
);
isTimeSystemUTCBased.value = timeSystem.isUTCBased;
}
return {
timeSystemKey,
timeSystemFormatter,
timeSystemDurationFormatter,
isTimeSystemUTCBased
};
}
function getFormatter(openmct, key) {
return openmct.telemetry.getValueFormatter({
format: key
}).formatter;
}

View File

@ -32,12 +32,13 @@ import moment from 'moment';
export default class UTCTimeFormat { export default class UTCTimeFormat {
constructor() { constructor() {
this.key = 'utc'; this.key = 'utc';
this.DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS'; this.DATE_DELIMITER = ' ';
this.DATE_FORMAT = `YYYY-MM-DD${this.DATE_DELIMITER}HH:mm:ss.SSS`;
this.DATE_FORMATS = { this.DATE_FORMATS = {
PRECISION_DEFAULT: this.DATE_FORMAT, PRECISION_DEFAULT: this.DATE_FORMAT,
PRECISION_DEFAULT_WITH_ZULU: this.DATE_FORMAT + 'Z', PRECISION_DEFAULT_WITH_ZULU: this.DATE_FORMAT + 'Z',
PRECISION_SECONDS: 'YYYY-MM-DD HH:mm:ss', PRECISION_SECONDS: `YYYY-MM-DD${this.DATE_DELIMITER}HH:mm:ss`,
PRECISION_MINUTES: 'YYYY-MM-DD HH:mm', PRECISION_MINUTES: `YYYY-MM-DD${this.DATE_DELIMITER}HH:mm`,
PRECISION_DAYS: 'YYYY-MM-DD', PRECISION_DAYS: 'YYYY-MM-DD',
PRECISION_SECONDS_TIME_ONLY: 'HH:mm:ss', PRECISION_SECONDS_TIME_ONLY: 'HH:mm:ss',
PRECISION_MINUTES_TIME_ONLY: 'HH:mm' PRECISION_MINUTES_TIME_ONLY: 'HH:mm'
@ -85,4 +86,8 @@ export default class UTCTimeFormat {
validate(text) { validate(text) {
return moment.utc(text, Object.values(this.DATE_FORMATS), true).isValid(); return moment.utc(text, Object.values(this.DATE_FORMATS), true).isValid();
} }
getDelimiter() {
return this.DATE_DELIMITER;
}
} }

View File

@ -229,6 +229,8 @@ $glyph-icon-grid-on: '\ea38';
$glyph-icon-grid-off: '\ea39'; $glyph-icon-grid-off: '\ea39';
$glyph-icon-camera: '\ea3a'; $glyph-icon-camera: '\ea3a';
$glyph-icon-folders-collapse: '\ea3b'; $glyph-icon-folders-collapse: '\ea3b';
$glyph-icon-multiline: '\ea3c';
$glyph-icon-singleline: '\ea3d';
$glyph-icon-activity: '\eb00'; $glyph-icon-activity: '\eb00';
$glyph-icon-activity-mode: '\eb01'; $glyph-icon-activity-mode: '\eb01';
$glyph-icon-autoflow-tabular: '\eb02'; $glyph-icon-autoflow-tabular: '\eb02';

View File

@ -42,501 +42,675 @@
.icon-alert-rect { .icon-alert-rect {
@include glyphBefore($glyph-icon-alert-rect); @include glyphBefore($glyph-icon-alert-rect);
} }
.icon-alert-triangle { .icon-alert-triangle {
@include glyphBefore($glyph-icon-alert-triangle); @include glyphBefore($glyph-icon-alert-triangle);
} }
.icon-arrow-up { .icon-arrow-up {
@include glyphBefore($glyph-icon-arrow-up); @include glyphBefore($glyph-icon-arrow-up);
} }
.icon-arrow-double-up { .icon-arrow-double-up {
@include glyphBefore($glyph-icon-arrow-double-up); @include glyphBefore($glyph-icon-arrow-double-up);
} }
.icon-arrow-tall-up { .icon-arrow-tall-up {
@include glyphBefore($glyph-icon-arrow-tall-up); @include glyphBefore($glyph-icon-arrow-tall-up);
} }
.icon-arrow-right { .icon-arrow-right {
@include glyphBefore($glyph-icon-arrow-right); @include glyphBefore($glyph-icon-arrow-right);
} }
.icon-arrow-right-equilateral { .icon-arrow-right-equilateral {
@include glyphBefore($glyph-icon-arrow-right-equilateral); @include glyphBefore($glyph-icon-arrow-right-equilateral);
} }
.icon-arrow-down { .icon-arrow-down {
@include glyphBefore($glyph-icon-arrow-down); @include glyphBefore($glyph-icon-arrow-down);
} }
.icon-arrow-double-down { .icon-arrow-double-down {
@include glyphBefore($glyph-icon-arrow-double-down); @include glyphBefore($glyph-icon-arrow-double-down);
} }
.icon-arrow-tall-down { .icon-arrow-tall-down {
@include glyphBefore($glyph-icon-arrow-tall-down); @include glyphBefore($glyph-icon-arrow-tall-down);
} }
.icon-arrow-left { .icon-arrow-left {
@include glyphBefore($glyph-icon-arrow-left); @include glyphBefore($glyph-icon-arrow-left);
} }
.icon-asterisk { .icon-asterisk {
@include glyphBefore($glyph-icon-asterisk); @include glyphBefore($glyph-icon-asterisk);
} }
.icon-bell { .icon-bell {
@include glyphBefore($glyph-icon-bell); @include glyphBefore($glyph-icon-bell);
} }
.icon-box-round-corners { .icon-box-round-corners {
@include glyphBefore($glyph-icon-box-round-corners); @include glyphBefore($glyph-icon-box-round-corners);
} }
.icon-box-with-arrow { .icon-box-with-arrow {
@include glyphBefore($glyph-icon-box-with-arrow); @include glyphBefore($glyph-icon-box-with-arrow);
} }
.icon-check { .icon-check {
@include glyphBefore($glyph-icon-check); @include glyphBefore($glyph-icon-check);
} }
.icon-connectivity { .icon-connectivity {
@include glyphBefore($glyph-icon-connectivity); @include glyphBefore($glyph-icon-connectivity);
} }
.icon-database-in-brackets { .icon-database-in-brackets {
@include glyphBefore($glyph-icon-database-in-brackets); @include glyphBefore($glyph-icon-database-in-brackets);
} }
.icon-eye-open { .icon-eye-open {
@include glyphBefore($glyph-icon-eye-open); @include glyphBefore($glyph-icon-eye-open);
} }
.icon-gear { .icon-gear {
@include glyphBefore($glyph-icon-gear); @include glyphBefore($glyph-icon-gear);
} }
.icon-gear-after { .icon-gear-after {
@include glyphAfter($glyph-icon-gear); @include glyphAfter($glyph-icon-gear);
} }
.icon-hourglass { .icon-hourglass {
@include glyphBefore($glyph-icon-hourglass); @include glyphBefore($glyph-icon-hourglass);
} }
.icon-info { .icon-info {
@include glyphBefore($glyph-icon-info); @include glyphBefore($glyph-icon-info);
} }
.icon-link { .icon-link {
@include glyphBefore($glyph-icon-link); @include glyphBefore($glyph-icon-link);
} }
.icon-lock { .icon-lock {
@include glyphBefore($glyph-icon-lock); @include glyphBefore($glyph-icon-lock);
} }
.icon-minus { .icon-minus {
@include glyphBefore($glyph-icon-minus); @include glyphBefore($glyph-icon-minus);
} }
.icon-people { .icon-people {
@include glyphBefore($glyph-icon-people); @include glyphBefore($glyph-icon-people);
} }
.icon-person { .icon-person {
@include glyphBefore($glyph-icon-person); @include glyphBefore($glyph-icon-person);
} }
.icon-plus { .icon-plus {
@include glyphBefore($glyph-icon-plus); @include glyphBefore($glyph-icon-plus);
} }
.icon-plus-in-rect { .icon-plus-in-rect {
@include glyphBefore($glyph-icon-plus-in-rect); @include glyphBefore($glyph-icon-plus-in-rect);
} }
.icon-trash { .icon-trash {
@include glyphBefore($glyph-icon-trash); @include glyphBefore($glyph-icon-trash);
} }
.icon-x { .icon-x {
@include glyphBefore($glyph-icon-x); @include glyphBefore($glyph-icon-x);
} }
.icon-brackets { .icon-brackets {
@include glyphBefore($glyph-icon-brackets); @include glyphBefore($glyph-icon-brackets);
} }
.icon-crosshair { .icon-crosshair {
@include glyphBefore($glyph-icon-crosshair); @include glyphBefore($glyph-icon-crosshair);
} }
.icon-grippy { .icon-grippy {
@include glyphBefore($glyph-icon-grippy); @include glyphBefore($glyph-icon-grippy);
} }
.icon-grid { .icon-grid {
@include glyphBefore($glyph-icon-grid); @include glyphBefore($glyph-icon-grid);
} }
.icon-grippy-ew { .icon-grippy-ew {
@include glyphBefore($glyph-icon-grippy-ew); @include glyphBefore($glyph-icon-grippy-ew);
} }
.icon-columns { .icon-columns {
@include glyphBefore($glyph-icon-columns); @include glyphBefore($glyph-icon-columns);
} }
.icon-rows { .icon-rows {
@include glyphBefore($glyph-icon-rows); @include glyphBefore($glyph-icon-rows);
} }
.icon-filter { .icon-filter {
@include glyphBefore($glyph-icon-filter); @include glyphBefore($glyph-icon-filter);
} }
.icon-filter-outline { .icon-filter-outline {
@include glyphBefore($glyph-icon-filter-outline); @include glyphBefore($glyph-icon-filter-outline);
} }
.icon-suitcase { .icon-suitcase {
@include glyphBefore($glyph-icon-suitcase); @include glyphBefore($glyph-icon-suitcase);
} }
.icon-cursor-lock { .icon-cursor-lock {
@include glyphBefore($glyph-icon-cursor-lock); @include glyphBefore($glyph-icon-cursor-lock);
} }
.icon-flag { .icon-flag {
@include glyphBefore($glyph-icon-flag); @include glyphBefore($glyph-icon-flag);
} }
.icon-eye-disabled { .icon-eye-disabled {
@include glyphBefore($glyph-icon-eye-disabled); @include glyphBefore($glyph-icon-eye-disabled);
} }
.icon-notebook-page { .icon-notebook-page {
@include glyphBefore($glyph-icon-notebook-page); @include glyphBefore($glyph-icon-notebook-page);
} }
.icon-unlocked { .icon-unlocked {
@include glyphBefore($glyph-icon-unlocked); @include glyphBefore($glyph-icon-unlocked);
} }
.icon-circle { .icon-circle {
@include glyphBefore($glyph-icon-circle); @include glyphBefore($glyph-icon-circle);
} }
.icon-draft { .icon-draft {
@include glyphBefore($glyph-icon-draft); @include glyphBefore($glyph-icon-draft);
} }
.icon-question-mark { .icon-question-mark {
@include glyphBefore($glyph-icon-question-mark); @include glyphBefore($glyph-icon-question-mark);
} }
.icon-circle-slash { .icon-circle-slash {
@include glyphBefore($glyph-icon-circle-slash); @include glyphBefore($glyph-icon-circle-slash);
} }
.icon-status-poll-check { .icon-status-poll-check {
@include glyphBefore($glyph-icon-status-poll-check); @include glyphBefore($glyph-icon-status-poll-check);
} }
.icon-status-poll-caution { .icon-status-poll-caution {
@include glyphBefore($glyph-icon-status-poll-caution); @include glyphBefore($glyph-icon-status-poll-caution);
} }
.icon-status-poll-circle-slash { .icon-status-poll-circle-slash {
@include glyphBefore($glyph-icon-status-poll-circle-slash); @include glyphBefore($glyph-icon-status-poll-circle-slash);
} }
.icon-status-poll-question-mark { .icon-status-poll-question-mark {
@include glyphBefore($glyph-icon-status-poll-question-mark); @include glyphBefore($glyph-icon-status-poll-question-mark);
} }
.icon-status-poll-edit { .icon-status-poll-edit {
@include glyphBefore($glyph-icon-status-poll-edit); @include glyphBefore($glyph-icon-status-poll-edit);
} }
.icon-stale { .icon-stale {
@include glyphBefore($glyph-icon-stale); @include glyphBefore($glyph-icon-stale);
} }
.icon-arrows-right-left { .icon-arrows-right-left {
@include glyphBefore($glyph-icon-arrows-right-left); @include glyphBefore($glyph-icon-arrows-right-left);
} }
.icon-arrows-up-down { .icon-arrows-up-down {
@include glyphBefore($glyph-icon-arrows-up-down); @include glyphBefore($glyph-icon-arrows-up-down);
} }
.icon-bullet { .icon-bullet {
@include glyphBefore($glyph-icon-bullet); @include glyphBefore($glyph-icon-bullet);
} }
.icon-calendar { .icon-calendar {
@include glyphBefore($glyph-icon-calendar); @include glyphBefore($glyph-icon-calendar);
} }
.icon-chain-links { .icon-chain-links {
@include glyphBefore($glyph-icon-chain-links); @include glyphBefore($glyph-icon-chain-links);
} }
.icon-download { .icon-download {
@include glyphBefore($glyph-icon-download); @include glyphBefore($glyph-icon-download);
} }
.icon-duplicate { .icon-duplicate {
@include glyphBefore($glyph-icon-duplicate); @include glyphBefore($glyph-icon-duplicate);
} }
.icon-folder-new { .icon-folder-new {
@include glyphBefore($glyph-icon-folder-new); @include glyphBefore($glyph-icon-folder-new);
} }
.icon-fullscreen-collapse { .icon-fullscreen-collapse {
@include glyphBefore($glyph-icon-fullscreen-collapse); @include glyphBefore($glyph-icon-fullscreen-collapse);
} }
.icon-fullscreen-expand { .icon-fullscreen-expand {
@include glyphBefore($glyph-icon-fullscreen-expand); @include glyphBefore($glyph-icon-fullscreen-expand);
} }
.icon-layers { .icon-layers {
@include glyphBefore($glyph-icon-layers); @include glyphBefore($glyph-icon-layers);
} }
.icon-line-horz { .icon-line-horz {
@include glyphBefore($glyph-icon-line-horz); @include glyphBefore($glyph-icon-line-horz);
} }
.icon-magnify { .icon-magnify {
@include glyphBefore($glyph-icon-magnify); @include glyphBefore($glyph-icon-magnify);
} }
.icon-magnify-in { .icon-magnify-in {
@include glyphBefore($glyph-icon-magnify-in); @include glyphBefore($glyph-icon-magnify-in);
} }
.icon-magnify-out { .icon-magnify-out {
@include glyphBefore($glyph-icon-magnify-out); @include glyphBefore($glyph-icon-magnify-out);
} }
.icon-menu-hamburger { .icon-menu-hamburger {
@include glyphBefore($glyph-icon-menu-hamburger); @include glyphBefore($glyph-icon-menu-hamburger);
} }
.icon-move { .icon-move {
@include glyphBefore($glyph-icon-move); @include glyphBefore($glyph-icon-move);
} }
.icon-new-window { .icon-new-window {
@include glyphBefore($glyph-icon-new-window); @include glyphBefore($glyph-icon-new-window);
} }
.icon-paint-bucket { .icon-paint-bucket {
@include glyphBefore($glyph-icon-paint-bucket); @include glyphBefore($glyph-icon-paint-bucket);
} }
.icon-pencil { .icon-pencil {
@include glyphBefore($glyph-icon-pencil); @include glyphBefore($glyph-icon-pencil);
} }
.icon-pencil-in-brackets { .icon-pencil-in-brackets {
@include glyphBefore($glyph-icon-pencil-in-brackets); @include glyphBefore($glyph-icon-pencil-in-brackets);
} }
.icon-play { .icon-play {
@include glyphBefore($glyph-icon-play); @include glyphBefore($glyph-icon-play);
} }
.icon-pause { .icon-pause {
@include glyphBefore($glyph-icon-pause); @include glyphBefore($glyph-icon-pause);
} }
.icon-plot-resource { .icon-plot-resource {
@include glyphBefore($glyph-icon-plot-resource); @include glyphBefore($glyph-icon-plot-resource);
} }
.icon-pointer-left { .icon-pointer-left {
@include glyphBefore($glyph-icon-pointer-left); @include glyphBefore($glyph-icon-pointer-left);
} }
.icon-pointer-right { .icon-pointer-right {
@include glyphBefore($glyph-icon-pointer-right); @include glyphBefore($glyph-icon-pointer-right);
} }
.icon-refresh { .icon-refresh {
@include glyphBefore($glyph-icon-refresh); @include glyphBefore($glyph-icon-refresh);
} }
.icon-save { .icon-save {
@include glyphBefore($glyph-icon-save); @include glyphBefore($glyph-icon-save);
} }
.icon-save-as { .icon-save-as {
@include glyphBefore($glyph-icon-save-as); @include glyphBefore($glyph-icon-save-as);
} }
.icon-sine { .icon-sine {
@include glyphBefore($glyph-icon-sine); @include glyphBefore($glyph-icon-sine);
} }
.icon-font { .icon-font {
@include glyphBefore($glyph-icon-font); @include glyphBefore($glyph-icon-font);
} }
.icon-thumbs-strip { .icon-thumbs-strip {
@include glyphBefore($glyph-icon-thumbs-strip); @include glyphBefore($glyph-icon-thumbs-strip);
} }
.icon-two-parts-both { .icon-two-parts-both {
@include glyphBefore($glyph-icon-two-parts-both); @include glyphBefore($glyph-icon-two-parts-both);
} }
.icon-two-parts-one-only { .icon-two-parts-one-only {
@include glyphBefore($glyph-icon-two-parts-one-only); @include glyphBefore($glyph-icon-two-parts-one-only);
} }
.icon-resync { .icon-resync {
@include glyphBefore($glyph-icon-resync); @include glyphBefore($glyph-icon-resync);
} }
.icon-reset { .icon-reset {
@include glyphBefore($glyph-icon-reset); @include glyphBefore($glyph-icon-reset);
} }
.icon-x-in-circle { .icon-x-in-circle {
@include glyphBefore($glyph-icon-x-in-circle); @include glyphBefore($glyph-icon-x-in-circle);
} }
.icon-brightness { .icon-brightness {
@include glyphBefore($glyph-icon-brightness); @include glyphBefore($glyph-icon-brightness);
} }
.icon-contrast { .icon-contrast {
@include glyphBefore($glyph-icon-contrast); @include glyphBefore($glyph-icon-contrast);
} }
.icon-expand { .icon-expand {
@include glyphBefore($glyph-icon-expand); @include glyphBefore($glyph-icon-expand);
} }
.icon-list-view { .icon-list-view {
@include glyphBefore($glyph-icon-list-view); @include glyphBefore($glyph-icon-list-view);
} }
.icon-grid-snap-to { .icon-grid-snap-to {
@include glyphBefore($glyph-icon-grid-snap-to); @include glyphBefore($glyph-icon-grid-snap-to);
} }
.icon-grid-snap-no { .icon-grid-snap-no {
@include glyphBefore($glyph-icon-grid-snap-no); @include glyphBefore($glyph-icon-grid-snap-no);
} }
.icon-frame-show { .icon-frame-show {
@include glyphBefore($glyph-icon-frame-show); @include glyphBefore($glyph-icon-frame-show);
} }
.icon-frame-hide { .icon-frame-hide {
@include glyphBefore($glyph-icon-frame-hide); @include glyphBefore($glyph-icon-frame-hide);
} }
.icon-import { .icon-import {
@include glyphBefore($glyph-icon-import); @include glyphBefore($glyph-icon-import);
} }
.icon-export { .icon-export {
@include glyphBefore($glyph-icon-export); @include glyphBefore($glyph-icon-export);
} }
.icon-font-size { .icon-font-size {
@include glyphBefore($glyph-icon-font-size); @include glyphBefore($glyph-icon-font-size);
} }
.icon-clear-data { .icon-clear-data {
@include glyphBefore($glyph-icon-clear-data); @include glyphBefore($glyph-icon-clear-data);
} }
.icon-history { .icon-history {
@include glyphBefore($glyph-icon-history); @include glyphBefore($glyph-icon-history);
} }
.icon-arrow-nav-to-parent { .icon-arrow-nav-to-parent {
@include glyphBefore($glyph-icon-arrow-nav-to-parent); @include glyphBefore($glyph-icon-arrow-nav-to-parent);
} }
.icon-crosshair-in-circle { .icon-crosshair-in-circle {
@include glyphBefore($glyph-icon-crosshair-in-circle); @include glyphBefore($glyph-icon-crosshair-in-circle);
} }
.icon-target { .icon-target {
@include glyphBefore($glyph-icon-target); @include glyphBefore($glyph-icon-target);
} }
.icon-items-collapse { .icon-items-collapse {
@include glyphBefore($glyph-icon-items-collapse); @include glyphBefore($glyph-icon-items-collapse);
} }
.icon-items-expand { .icon-items-expand {
@include glyphBefore($glyph-icon-items-expand); @include glyphBefore($glyph-icon-items-expand);
} }
.icon-3-dots { .icon-3-dots {
@include glyphBefore($glyph-icon-3-dots); @include glyphBefore($glyph-icon-3-dots);
} }
.icon-grid-on { .icon-grid-on {
@include glyphBefore($glyph-icon-grid-on); @include glyphBefore($glyph-icon-grid-on);
} }
.icon-grid-off { .icon-grid-off {
@include glyphBefore($glyph-icon-grid-off); @include glyphBefore($glyph-icon-grid-off);
} }
.icon-camera { .icon-camera {
@include glyphBefore($glyph-icon-camera); @include glyphBefore($glyph-icon-camera);
} }
.icon-folders-collapse { .icon-folders-collapse {
@include glyphBefore($glyph-icon-folders-collapse); @include glyphBefore($glyph-icon-folders-collapse);
} }
.icon-multiline {
@include glyphBefore($glyph-icon-multiline);
}
.icon-singleline {
@include glyphBefore($glyph-icon-singleline);
}
.icon-activity { .icon-activity {
@include glyphBefore($glyph-icon-activity); @include glyphBefore($glyph-icon-activity);
} }
.icon-activity-mode { .icon-activity-mode {
@include glyphBefore($glyph-icon-activity-mode); @include glyphBefore($glyph-icon-activity-mode);
} }
.icon-autoflow-tabular { .icon-autoflow-tabular {
@include glyphBefore($glyph-icon-autoflow-tabular); @include glyphBefore($glyph-icon-autoflow-tabular);
} }
.icon-clock { .icon-clock {
@include glyphBefore($glyph-icon-clock); @include glyphBefore($glyph-icon-clock);
} }
.icon-database { .icon-database {
@include glyphBefore($glyph-icon-database); @include glyphBefore($glyph-icon-database);
} }
.icon-database-query { .icon-database-query {
@include glyphBefore($glyph-icon-database-query); @include glyphBefore($glyph-icon-database-query);
} }
.icon-dataset { .icon-dataset {
@include glyphBefore($glyph-icon-dataset); @include glyphBefore($glyph-icon-dataset);
} }
.icon-datatable { .icon-datatable {
@include glyphBefore($glyph-icon-datatable); @include glyphBefore($glyph-icon-datatable);
} }
.icon-dictionary { .icon-dictionary {
@include glyphBefore($glyph-icon-dictionary); @include glyphBefore($glyph-icon-dictionary);
} }
.icon-folder { .icon-folder {
@include glyphBefore($glyph-icon-folder); @include glyphBefore($glyph-icon-folder);
} }
.icon-image { .icon-image {
@include glyphBefore($glyph-icon-image); @include glyphBefore($glyph-icon-image);
} }
.icon-layout { .icon-layout {
@include glyphBefore($glyph-icon-layout); @include glyphBefore($glyph-icon-layout);
} }
.icon-object { .icon-object {
@include glyphBefore($glyph-icon-object); @include glyphBefore($glyph-icon-object);
} }
.icon-object-unknown { .icon-object-unknown {
@include glyphBefore($glyph-icon-object-unknown); @include glyphBefore($glyph-icon-object-unknown);
} }
.icon-packet { .icon-packet {
@include glyphBefore($glyph-icon-packet); @include glyphBefore($glyph-icon-packet);
} }
.icon-page { .icon-page {
@include glyphBefore($glyph-icon-page); @include glyphBefore($glyph-icon-page);
} }
.icon-plot-overlay { .icon-plot-overlay {
@include glyphBefore($glyph-icon-plot-overlay); @include glyphBefore($glyph-icon-plot-overlay);
} }
.icon-plot-stacked { .icon-plot-stacked {
@include glyphBefore($glyph-icon-plot-stacked); @include glyphBefore($glyph-icon-plot-stacked);
} }
.icon-session { .icon-session {
@include glyphBefore($glyph-icon-session); @include glyphBefore($glyph-icon-session);
} }
.icon-tabular { .icon-tabular {
@include glyphBefore($glyph-icon-tabular); @include glyphBefore($glyph-icon-tabular);
} }
.icon-tabular-lad { .icon-tabular-lad {
@include glyphBefore($glyph-icon-tabular-lad); @include glyphBefore($glyph-icon-tabular-lad);
} }
.icon-tabular-lad-set { .icon-tabular-lad-set {
@include glyphBefore($glyph-icon-tabular-lad-set); @include glyphBefore($glyph-icon-tabular-lad-set);
} }
.icon-tabular-realtime { .icon-tabular-realtime {
@include glyphBefore($glyph-icon-tabular-realtime); @include glyphBefore($glyph-icon-tabular-realtime);
} }
.icon-tabular-scrolling { .icon-tabular-scrolling {
@include glyphBefore($glyph-icon-tabular-scrolling); @include glyphBefore($glyph-icon-tabular-scrolling);
} }
.icon-telemetry { .icon-telemetry {
@include glyphBefore($glyph-icon-telemetry); @include glyphBefore($glyph-icon-telemetry);
} }
.icon-timeline { .icon-timeline {
@include glyphBefore($glyph-icon-timeline); @include glyphBefore($glyph-icon-timeline);
} }
.icon-timer { .icon-timer {
@include glyphBefore($glyph-icon-timer); @include glyphBefore($glyph-icon-timer);
} }
.icon-topic { .icon-topic {
@include glyphBefore($glyph-icon-topic); @include glyphBefore($glyph-icon-topic);
} }
.icon-box-with-dashed-lines { .icon-box-with-dashed-lines {
@include glyphBefore($glyph-icon-box-with-dashed-lines); @include glyphBefore($glyph-icon-box-with-dashed-lines);
} }
.icon-summary-widget { .icon-summary-widget {
@include glyphBefore($glyph-icon-summary-widget); @include glyphBefore($glyph-icon-summary-widget);
} }
.icon-notebook { .icon-notebook {
@include glyphBefore($glyph-icon-notebook); @include glyphBefore($glyph-icon-notebook);
} }
.icon-tabs-view { .icon-tabs-view {
@include glyphBefore($glyph-icon-tabs-view); @include glyphBefore($glyph-icon-tabs-view);
} }
.icon-flexible-layout { .icon-flexible-layout {
@include glyphBefore($glyph-icon-flexible-layout); @include glyphBefore($glyph-icon-flexible-layout);
} }
.icon-generator-telemetry { .icon-generator-telemetry {
@include glyphBefore($glyph-icon-generator-telemetry); @include glyphBefore($glyph-icon-generator-telemetry);
} }
.icon-generator-events { .icon-generator-events {
@include glyphBefore($glyph-icon-generator-events); @include glyphBefore($glyph-icon-generator-events);
} }
.icon-gauge { .icon-gauge {
@include glyphBefore($glyph-icon-gauge); @include glyphBefore($glyph-icon-gauge);
} }
.icon-spectra { .icon-spectra {
@include glyphBefore($glyph-icon-spectra); @include glyphBefore($glyph-icon-spectra);
} }
.icon-spectra-telemetry { .icon-spectra-telemetry {
@include glyphBefore($glyph-icon-spectra-telemetry); @include glyphBefore($glyph-icon-spectra-telemetry);
} }
.icon-command { .icon-command {
@include glyphBefore($glyph-icon-command); @include glyphBefore($glyph-icon-command);
} }
.icon-conditional { .icon-conditional {
@include glyphBefore($glyph-icon-conditional); @include glyphBefore($glyph-icon-conditional);
} }
.icon-condition-widget { .icon-condition-widget {
@include glyphBefore($glyph-icon-condition-widget); @include glyphBefore($glyph-icon-condition-widget);
} }
.icon-alphanumeric { .icon-alphanumeric {
@include glyphBefore($glyph-icon-alphanumeric); @include glyphBefore($glyph-icon-alphanumeric);
} }
.icon-image-telemetry { .icon-image-telemetry {
@include glyphBefore($glyph-icon-image-telemetry); @include glyphBefore($glyph-icon-image-telemetry);
} }
.icon-telemetry-aggregate { .icon-telemetry-aggregate {
@include glyphBefore($glyph-icon-telemetry-aggregate); @include glyphBefore($glyph-icon-telemetry-aggregate);
} }
.icon-bar-chart { .icon-bar-chart {
@include glyphBefore($glyph-icon-bar-chart); @include glyphBefore($glyph-icon-bar-chart);
} }
.icon-map { .icon-map {
@include glyphBefore($glyph-icon-map); @include glyphBefore($glyph-icon-map);
} }
.icon-plan { .icon-plan {
@include glyphBefore($glyph-icon-plan); @include glyphBefore($glyph-icon-plan);
} }
.icon-timelist { .icon-timelist {
@include glyphBefore($glyph-icon-timelist); @include glyphBefore($glyph-icon-timelist);
} }
.icon-notebook-shift-log { .icon-notebook-shift-log {
@include glyphBefore($glyph-icon-notebook-shift-log); @include glyphBefore($glyph-icon-notebook-shift-log);
} }
.icon-plot-scatter { .icon-plot-scatter {
@include glyphBefore($glyph-icon-plot-scatter); @include glyphBefore($glyph-icon-plot-scatter);
} }
@ -546,18 +720,23 @@
.icon-filter-12px { .icon-filter-12px {
@include glyphBefore($glyph-icon-filter, 'symbolsfont-12px'); @include glyphBefore($glyph-icon-filter, 'symbolsfont-12px');
} }
.icon-filter-outline-12px { .icon-filter-outline-12px {
@include glyphBefore($glyph-icon-filter-outline, 'symbolsfont-12px'); @include glyphBefore($glyph-icon-filter-outline, 'symbolsfont-12px');
} }
.icon-crosshair-12px { .icon-crosshair-12px {
@include glyphBefore($glyph-icon-crosshair, 'symbolsfont-12px'); @include glyphBefore($glyph-icon-crosshair, 'symbolsfont-12px');
} }
.icon-folder-12px { .icon-folder-12px {
@include glyphBefore($glyph-icon-folder, 'symbolsfont-12px'); @include glyphBefore($glyph-icon-folder, 'symbolsfont-12px');
} }
.icon-list-view-12px { .icon-list-view-12px {
@include glyphBefore($glyph-icon-list-view, 'symbolsfont-12px'); @include glyphBefore($glyph-icon-list-view, 'symbolsfont-12px');
} }
.icon-grippy-12px { .icon-grippy-12px {
@include glyphBefore($glyph-icon-grippy, 'symbolsfont-12px'); @include glyphBefore($glyph-icon-grippy, 'symbolsfont-12px');
} }
@ -566,159 +745,211 @@
.bg-icon-alert-rect { .bg-icon-alert-rect {
@include glyphBg($bg-icon-alert-rect); @include glyphBg($bg-icon-alert-rect);
} }
.bg-icon-alert-triangle { .bg-icon-alert-triangle {
@include glyphBg($bg-icon-alert-triangle); @include glyphBg($bg-icon-alert-triangle);
} }
.bg-icon-bell { .bg-icon-bell {
@include glyphBg($bg-icon-bell); @include glyphBg($bg-icon-bell);
} }
.bg-icon-info { .bg-icon-info {
@include glyphBg($bg-icon-info); @include glyphBg($bg-icon-info);
} }
.bg-icon-plus { .bg-icon-plus {
@include glyphBg($bg-icon-plus); @include glyphBg($bg-icon-plus);
} }
.bg-icon-grippy-ew { .bg-icon-grippy-ew {
@include glyphBg($bg-icon-grippy-ew); @include glyphBg($bg-icon-grippy-ew);
} }
.bg-icon-chain-links { .bg-icon-chain-links {
@include glyphBg($bg-icon-chain-links); @include glyphBg($bg-icon-chain-links);
} }
.bg-icon-clock { .bg-icon-clock {
@include glyphBg($bg-icon-clock); @include glyphBg($bg-icon-clock);
} }
.bg-icon-database { .bg-icon-database {
@include glyphBg($bg-icon-database); @include glyphBg($bg-icon-database);
} }
.bg-icon-database-query { .bg-icon-database-query {
@include glyphBg($bg-icon-database-query); @include glyphBg($bg-icon-database-query);
} }
.bg-icon-dataset { .bg-icon-dataset {
@include glyphBg($bg-icon-dataset); @include glyphBg($bg-icon-dataset);
} }
.bg-icon-datatable { .bg-icon-datatable {
@include glyphBg($bg-icon-datatable); @include glyphBg($bg-icon-datatable);
} }
.bg-icon-dictionary { .bg-icon-dictionary {
@include glyphBg($bg-icon-dictionary); @include glyphBg($bg-icon-dictionary);
} }
.bg-icon-folder { .bg-icon-folder {
@include glyphBg($bg-icon-folder); @include glyphBg($bg-icon-folder);
} }
.bg-icon-image { .bg-icon-image {
@include glyphBg($bg-icon-image); @include glyphBg($bg-icon-image);
} }
.bg-icon-layout { .bg-icon-layout {
@include glyphBg($bg-icon-layout); @include glyphBg($bg-icon-layout);
} }
.bg-icon-object { .bg-icon-object {
@include glyphBg($bg-icon-object); @include glyphBg($bg-icon-object);
} }
.bg-icon-object-unknown { .bg-icon-object-unknown {
@include glyphBg($bg-icon-object-unknown); @include glyphBg($bg-icon-object-unknown);
} }
.bg-icon-packet { .bg-icon-packet {
@include glyphBg($bg-icon-packet); @include glyphBg($bg-icon-packet);
} }
.bg-icon-page { .bg-icon-page {
@include glyphBg($bg-icon-page); @include glyphBg($bg-icon-page);
} }
.bg-icon-plot-overlay { .bg-icon-plot-overlay {
@include glyphBg($bg-icon-plot-overlay); @include glyphBg($bg-icon-plot-overlay);
} }
.bg-icon-plot-stacked { .bg-icon-plot-stacked {
@include glyphBg($bg-icon-plot-stacked); @include glyphBg($bg-icon-plot-stacked);
} }
.bg-icon-session { .bg-icon-session {
@include glyphBg($bg-icon-session); @include glyphBg($bg-icon-session);
} }
.bg-icon-tabular { .bg-icon-tabular {
@include glyphBg($bg-icon-tabular); @include glyphBg($bg-icon-tabular);
} }
.bg-icon-tabular-lad { .bg-icon-tabular-lad {
@include glyphBg($bg-icon-tabular-lad); @include glyphBg($bg-icon-tabular-lad);
} }
.bg-icon-tabular-lad-set { .bg-icon-tabular-lad-set {
@include glyphBg($bg-icon-tabular-lad-set); @include glyphBg($bg-icon-tabular-lad-set);
} }
.bg-icon-tabular-scrolling { .bg-icon-tabular-scrolling {
@include glyphBg($bg-icon-tabular-scrolling); @include glyphBg($bg-icon-tabular-scrolling);
} }
.bg-icon-telemetry { .bg-icon-telemetry {
@include glyphBg($bg-icon-telemetry); @include glyphBg($bg-icon-telemetry);
} }
.bg-icon-timeline { .bg-icon-timeline {
@include glyphBg($bg-icon-timeline); @include glyphBg($bg-icon-timeline);
} }
.bg-icon-timer { .bg-icon-timer {
@include glyphBg($bg-icon-timer); @include glyphBg($bg-icon-timer);
} }
.bg-icon-box-with-dashed-lines { .bg-icon-box-with-dashed-lines {
@include glyphBg($bg-icon-box-with-dashed-lines); @include glyphBg($bg-icon-box-with-dashed-lines);
} }
.bg-icon-summary-widget { .bg-icon-summary-widget {
@include glyphBg($bg-icon-summary-widget); @include glyphBg($bg-icon-summary-widget);
} }
.bg-icon-notebook { .bg-icon-notebook {
@include glyphBg($bg-icon-notebook); @include glyphBg($bg-icon-notebook);
} }
.bg-icon-tabs-view { .bg-icon-tabs-view {
@include glyphBg($bg-icon-tabs-view); @include glyphBg($bg-icon-tabs-view);
} }
.bg-icon-flexible-layout { .bg-icon-flexible-layout {
@include glyphBg($bg-icon-flexible-layout); @include glyphBg($bg-icon-flexible-layout);
} }
.bg-icon-generator-telemetry { .bg-icon-generator-telemetry {
@include glyphBg($bg-icon-generator-telemetry); @include glyphBg($bg-icon-generator-telemetry);
} }
.bg-icon-generator-events { .bg-icon-generator-events {
@include glyphBg($bg-icon-generator-events); @include glyphBg($bg-icon-generator-events);
} }
.bg-icon-gauge { .bg-icon-gauge {
@include glyphBg($bg-icon-gauge); @include glyphBg($bg-icon-gauge);
} }
.bg-icon-spectra { .bg-icon-spectra {
@include glyphBg($bg-icon-spectra); @include glyphBg($bg-icon-spectra);
} }
.bg-icon-spectra-telemetry { .bg-icon-spectra-telemetry {
@include glyphBg($bg-icon-spectra-telemetry); @include glyphBg($bg-icon-spectra-telemetry);
} }
.bg-icon-command { .bg-icon-command {
@include glyphBg($bg-icon-command); @include glyphBg($bg-icon-command);
} }
.bg-icon-conditional { .bg-icon-conditional {
@include glyphBg($bg-icon-conditional); @include glyphBg($bg-icon-conditional);
} }
.bg-icon-condition-widget { .bg-icon-condition-widget {
@include glyphBg($bg-icon-condition-widget); @include glyphBg($bg-icon-condition-widget);
} }
.bg-icon-bar-chart { .bg-icon-bar-chart {
@include glyphBg($bg-icon-bar-chart); @include glyphBg($bg-icon-bar-chart);
} }
.bg-icon-map { .bg-icon-map {
@include glyphBg($bg-icon-map); @include glyphBg($bg-icon-map);
} }
.bg-icon-plan { .bg-icon-plan {
@include glyphBg($bg-icon-plan); @include glyphBg($bg-icon-plan);
} }
.bg-icon-timelist { .bg-icon-timelist {
@include glyphBg($bg-icon-timelist); @include glyphBg($bg-icon-timelist);
} }
.bg-icon-plot-scatter { .bg-icon-plot-scatter {
@include glyphBg($bg-icon-plot-scatter); @include glyphBg($bg-icon-plot-scatter);
} }
.bg-icon-notebook-shift-log { .bg-icon-notebook-shift-log {
@include glyphBg($bg-icon-notebook-shift-log); @include glyphBg($bg-icon-notebook-shift-log);
} }
.bg-icon-telemetry-aggregate { .bg-icon-telemetry-aggregate {
@include glyphBg($bg-icon-telemetry-aggregate); @include glyphBg($bg-icon-telemetry-aggregate);
} }
.bg-icon-trash { .bg-icon-trash {
@include glyphBg($bg-icon-trash); @include glyphBg($bg-icon-trash);
} }
.bg-icon-eye-open { .bg-icon-eye-open {
@include glyphBg($bg-icon-eye-open); @include glyphBg($bg-icon-eye-open);
} }
.bg-icon-camera { .bg-icon-camera {
@include glyphBg($bg-icon-camera); @include glyphBg($bg-icon-camera);
} }

File diff suppressed because it is too large Load Diff

View File

@ -123,6 +123,8 @@
<glyph unicode="&#xea39;" glyph-name="icon-grid-off" d="M256 344.6l128 157.6v9.8h8l104 128h-112v256h-128v-256h-256v-128h256v-167.4zM184 256h-184v-128h80l104 128zM768 423.4l-128-157.6v-9.8h-8l-104-128h112v-256h128v256h256v128h-256v167.4zM840 512h184v128h-80l-104-128zM832 896l-832-1024h192l832 1024h-192z" /> <glyph unicode="&#xea39;" glyph-name="icon-grid-off" d="M256 344.6l128 157.6v9.8h8l104 128h-112v256h-128v-256h-256v-128h256v-167.4zM184 256h-184v-128h80l104 128zM768 423.4l-128-157.6v-9.8h-8l-104-128h112v-256h128v256h256v128h-256v167.4zM840 512h184v128h-80l-104-128zM832 896l-832-1024h192l832 1024h-192z" />
<glyph unicode="&#xea3a;" glyph-name="icon-camera" d="M896 640h-128l-128 256h-256l-128-256h-128c-70.601-0.227-127.773-57.399-128-127.978v-512.022c0.227-70.601 57.399-127.773 127.978-128h768.022c70.601 0.227 127.773 57.399 128 127.978v512.022c-0.227 70.601-57.399 127.773-127.978 128h-0.022zM512 32c-141.385 0-256 114.615-256 256s114.615 256 256 256c141.385 0 256-114.615 256-256v0c0-141.385-114.615-256-256-256v0z" /> <glyph unicode="&#xea3a;" glyph-name="icon-camera" d="M896 640h-128l-128 256h-256l-128-256h-128c-70.601-0.227-127.773-57.399-128-127.978v-512.022c0.227-70.601 57.399-127.773 127.978-128h768.022c70.601 0.227 127.773 57.399 128 127.978v512.022c-0.227 70.601-57.399 127.773-127.978 128h-0.022zM512 32c-141.385 0-256 114.615-256 256s114.615 256 256 256c141.385 0 256-114.615 256-256v0c0-141.385-114.615-256-256-256v0z" />
<glyph unicode="&#xea3b;" glyph-name="icon-folders-collapse" d="M896 576v-448c-0.215-70.606-57.394-127.785-127.979-128h-576.021c0.215-70.606 57.394-127.785 127.979-128h576.021c70.606 0.215 127.785 57.394 128 127.979v448.021c-0.215 70.606-57.394 127.785-127.979 128h-0.021zM832 192v448c-0.215 70.606-57.394 127.785-127.979 128h-192.021l-101.5 82.74c-24.88 24.9-74.040 45.26-109.24 45.26h-237.26c-35.305-0.102-63.898-28.695-64-63.99v-640.010c0.215-70.606 57.394-127.785 127.979-128h576.021c70.606 0.215 127.785 57.394 128 127.979v0.021zM128 252v516l256-260z" /> <glyph unicode="&#xea3b;" glyph-name="icon-folders-collapse" d="M896 576v-448c-0.215-70.606-57.394-127.785-127.979-128h-576.021c0.215-70.606 57.394-127.785 127.979-128h576.021c70.606 0.215 127.785 57.394 128 127.979v448.021c-0.215 70.606-57.394 127.785-127.979 128h-0.021zM832 192v448c-0.215 70.606-57.394 127.785-127.979 128h-192.021l-101.5 82.74c-24.88 24.9-74.040 45.26-109.24 45.26h-237.26c-35.305-0.102-63.898-28.695-64-63.99v-640.010c0.215-70.606 57.394-127.785 127.979-128h576.021c70.606 0.215 127.785 57.394 128 127.979v0.021zM128 252v516l256-260z" />
<glyph unicode="&#xea3c;" glyph-name="icon-multiline" d="M832.4 767.4c22.8 0 38-11.8 45-19 7-7 19-22.4 19-45v-640c0-22.8-11.8-38-19-45-7-7-22.4-19-45-19h-640c-22.8 0-38 11.8-45 19-7 7-19 22.4-19 45v640c0 22.8 11.8 38 19 45 7 7 22.4 19 45 19h640zM832.4 895.4h-640c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192v0zM256.4 575.4h512v-128h-512v128zM384.4 319.4h384v-128h-384v128z" />
<glyph unicode="&#xea3d;" glyph-name="icon-singleline" d="M832.4 895.4h-640c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM896.4 447.4h-512v-128h512v-256c0-22.8-11.8-38-19-45-7-7-22.4-19-45-19h-640c-22.8 0-38 11.8-45 19-7 7-19 22.4-19 45v640c0 22.8 11.8 38 19 45 7 7 22.4 19 45 19h640c22.8 0 38-11.8 45-19 7-7 19-22.4 19-45v-256z" />
<glyph unicode="&#xeb00;" glyph-name="icon-activity" d="M576 832h-256l320-320h-290.256c-44.264 76.516-126.99 128-221.744 128h-128v-512h128c94.754 0 177.48 51.484 221.744 128h290.256l-320-320h256l448 448-448 448z" /> <glyph unicode="&#xeb00;" glyph-name="icon-activity" d="M576 832h-256l320-320h-290.256c-44.264 76.516-126.99 128-221.744 128h-128v-512h128c94.754 0 177.48 51.484 221.744 128h290.256l-320-320h256l448 448-448 448z" />
<glyph unicode="&#xeb01;" glyph-name="icon-activity-mode" d="M512 896c-214.8 0-398.8-132.4-474.8-320h90.8c56.8 0 108-24.8 143-64h241l-192 192h256l320-320-320-320h-256l192 192h-241c-35-39.2-86.2-64-143-64h-90.8c76-187.6 259.8-320 474.8-320 282.8 0 512 229.2 512 512s-229.2 512-512 512z" /> <glyph unicode="&#xeb01;" glyph-name="icon-activity-mode" d="M512 896c-214.8 0-398.8-132.4-474.8-320h90.8c56.8 0 108-24.8 143-64h241l-192 192h256l320-320-320-320h-256l192 192h-241c-35-39.2-86.2-64-143-64h-90.8c76-187.6 259.8-320 474.8-320 282.8 0 512 229.2 512 512s-229.2 512-512 512z" />
<glyph unicode="&#xeb02;" glyph-name="icon-autoflow-tabular" d="M192 896c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h64v1024h-64zM384 896h256v-1024h-256v1024zM832 896h-64v-704h256v512c0 105.6-86.4 192-192 192z" /> <glyph unicode="&#xeb02;" glyph-name="icon-autoflow-tabular" d="M192 896c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h64v1024h-64zM384 896h256v-1024h-256v1024zM832 896h-64v-704h256v512c0 105.6-86.4 192-192 192z" />

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -1,16 +1,13 @@
/******************************************************** PROGRESS BAR */ /******************************************************** PROGRESS BAR */
@keyframes progressIndeterminate { @keyframes progressIndeterminate {
0% { 0% {
left: 0; transform:scaleX(0);
width: 0;
} }
70% { 90% {
left: 0; transform:scaleX(1);
width: 100%;
opacity: 1; opacity: 1;
} }
100% { 100% {
left: 100%;
opacity: 0; opacity: 0;
} }
} }
@ -24,11 +21,10 @@
&__bar { &__bar {
background: $colorProgressBar; background: $colorProgressBar;
height: 100%; transform-origin: left;
min-height: $progressBarMinH;
&.--indeterminate { &.--indeterminate {
position: absolute; @include abs();
animation: progressIndeterminate 1.5s ease-in infinite; animation: progressIndeterminate 1.5s ease-in infinite;
} }
} }

View File

@ -32,21 +32,25 @@
class="l-shell__head" class="l-shell__head"
:class="{ :class="{
'l-shell__head--expanded': headExpanded, 'l-shell__head--expanded': headExpanded,
'l-shell__head--minify-indicators': !headExpanded 'l-shell__head--minify-indicators': !headExpanded,
'l-shell__head--indicators-single-line': !indicatorsMultiline
}" }"
> >
<CreateButton class="l-shell__create-button" /> <CreateButton class="l-shell__create-button" />
<GrandSearch ref="grand-search" /> <GrandSearch ref="grand-search" />
<StatusIndicators /> <StatusIndicators ref="indicatorsComponent" />
<button <button
class="l-shell__head__collapse-button c-icon-button" class="l-shell__head__button"
:class=" :class="indicatorsMultilineCssClass"
headExpanded :aria-label="indicatorsMultilineLabel"
? 'l-shell__head__collapse-button--collapse' :title="indicatorsMultilineLabel"
: 'l-shell__head__collapse-button--expand' @click="toggleIndicatorsMultiline"
" ></button>
:aria-label="`Click to ${headExpanded ? 'collapse' : 'expand'} items`" <button
:title="`Click to ${headExpanded ? 'collapse' : 'expand'} items`" class="l-shell__head__button"
:class="headExpanded ? 'icon-items-collapse' : 'icon-items-expand'"
:aria-label="`Show ${headExpanded ? 'icon only' : 'icon and name'}`"
:title="`Show ${headExpanded ? 'icon only' : 'icon and name'}`"
@click="toggleShellHead" @click="toggleShellHead"
></button> ></button>
<NotificationBanner /> <NotificationBanner />
@ -167,6 +171,8 @@
</template> </template>
<script> <script>
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
import ObjectView from '../components/ObjectView.vue'; import ObjectView from '../components/ObjectView.vue';
import Inspector from '../inspector/InspectorPanel.vue'; import Inspector from '../inspector/InspectorPanel.vue';
import Toolbar from '../toolbar/ToolbarContainer.vue'; import Toolbar from '../toolbar/ToolbarContainer.vue';
@ -181,6 +187,10 @@ import GrandSearch from './search/GrandSearch.vue';
import NotificationBanner from './status-bar/NotificationBanner.vue'; import NotificationBanner from './status-bar/NotificationBanner.vue';
import StatusIndicators from './status-bar/StatusIndicators.vue'; import StatusIndicators from './status-bar/StatusIndicators.vue';
const SHELL_HEAD_LOCAL_STORAGE_KEY = 'openmct-shell-head';
const DEFAULT_HEAD_EXPANDED = true;
const DEFAULT_INDICATORS_MULTILINE = true;
export default { export default {
components: { components: {
Inspector, Inspector,
@ -198,13 +208,120 @@ export default {
RecentObjectsList RecentObjectsList
}, },
inject: ['openmct'], inject: ['openmct'],
data: function () { setup() {
let storedHeadProps = window.localStorage.getItem('openmct-shell-head'); let resizeObserver;
let headExpanded = true; let element;
if (storedHeadProps) {
headExpanded = JSON.parse(storedHeadProps).expanded; const storedHeadProps = localStorage.getItem(SHELL_HEAD_LOCAL_STORAGE_KEY);
const storedHeadPropsObject = JSON.parse(storedHeadProps);
const storedHeadExpanded = storedHeadPropsObject?.expanded;
const storedIndicatorsMultiline = storedHeadPropsObject?.multiline;
// template ref of StatusIndicators component
const indicatorsComponent = ref(null);
const width = ref(null);
const scrollWidth = ref(null);
const headExpanded = ref(storedHeadExpanded ?? DEFAULT_HEAD_EXPANDED);
const indicatorsMultiline = ref(storedIndicatorsMultiline ?? DEFAULT_INDICATORS_MULTILINE);
const isOverflowing = computed(() => scrollWidth.value > width.value);
const indicatorsMultilineCssClass = computed(() => {
const multilineClass = indicatorsMultiline.value ? 'icon-singleline' : 'icon-multiline';
const overflowingClass =
isOverflowing.value && !indicatorsMultiline.value
? 'c-button c-button--major'
: 'c-icon-button';
return `${multilineClass} ${overflowingClass}`;
});
const indicatorsMultilineLabel = computed(() => {
return `Display as ${indicatorsMultiline.value ? 'single line' : 'multiple lines'}`;
});
const initialHeadProps = JSON.stringify({
expanded: headExpanded.value,
multiline: indicatorsMultiline.value
});
if (initialHeadProps !== storedHeadProps) {
localStorage.setItem(SHELL_HEAD_LOCAL_STORAGE_KEY, initialHeadProps);
} }
onMounted(() => {
resizeObserver = new ResizeObserver((entries) => {
width.value = entries[0].target.clientWidth;
scrollWidth.value = entries[0].target.scrollWidth;
});
// indicatorsContainer is a template ref inside of indicatorsComponent
element = indicatorsComponent.value.$refs.indicatorsContainer;
if (!indicatorsMultiline.value) {
observeIndicatorsOverflow();
}
});
onUnmounted(() => {
resizeObserver.disconnect();
});
function observeIndicatorsOverflow() {
resizeObserver.observe(element);
}
function unObserveIndicatorsOverflow() {
resizeObserver.unobserve(element);
}
function checkIndicatorsElementWidths() {
if (!indicatorsMultiline.value) {
width.value = element.clientWidth;
scrollWidth.value = element.scrollWidth;
}
}
async function toggleShellHead() {
headExpanded.value = !headExpanded.value;
setLocalStorageShellHead();
// nextTick is used because the element width on toggle is updated using css
await nextTick();
checkIndicatorsElementWidths();
}
function toggleIndicatorsMultiline() {
indicatorsMultiline.value = !indicatorsMultiline.value;
setLocalStorageShellHead();
if (indicatorsMultiline.value) {
unObserveIndicatorsOverflow();
} else {
observeIndicatorsOverflow();
}
}
function setLocalStorageShellHead() {
localStorage.setItem(
SHELL_HEAD_LOCAL_STORAGE_KEY,
JSON.stringify({
expanded: headExpanded.value,
multiline: indicatorsMultiline.value
})
);
}
return {
indicatorsComponent,
isOverflowing,
headExpanded,
indicatorsMultiline,
indicatorsMultilineCssClass,
indicatorsMultilineLabel,
toggleIndicatorsMultiline,
toggleShellHead
};
},
data() {
return { return {
fullScreen: false, fullScreen: false,
conductorComponent: undefined, conductorComponent: undefined,
@ -213,7 +330,6 @@ export default {
actionCollection: undefined, actionCollection: undefined,
triggerSync: false, triggerSync: false,
triggerReset: false, triggerReset: false,
headExpanded,
isResizing: false, isResizing: false,
disableClearButton: false disableClearButton: false
}; };
@ -261,16 +377,6 @@ export default {
document.msExitFullscreen(); document.msExitFullscreen();
} }
}, },
toggleShellHead() {
this.headExpanded = !this.headExpanded;
window.localStorage.setItem(
'openmct-shell-head',
JSON.stringify({
expanded: this.headExpanded
})
);
},
fullScreenToggle() { fullScreenToggle() {
if (this.fullScreen) { if (this.fullScreen) {
this.fullScreen = false; this.fullScreen = false;

View File

@ -24,6 +24,7 @@
<div class="l-browse-bar__start"> <div class="l-browse-bar__start">
<button <button
v-if="hasParent" v-if="hasParent"
aria-label="Navigate up to parent"
class="l-browse-bar__nav-to-parent-button c-icon-button c-icon-button--major icon-arrow-nav-to-parent" class="l-browse-bar__nav-to-parent-button c-icon-button c-icon-button--major icon-arrow-nav-to-parent"
title="Navigate up to parent" title="Navigate up to parent"
@click="goToParent" @click="goToParent"

View File

@ -213,27 +213,14 @@
align-items: center; align-items: center;
background: $colorHeadBg; background: $colorHeadBg;
display: grid; display: grid;
grid-template-columns: min-content 1fr 3fr repeat(3, min-content); grid-template-columns: min-content 1fr 3fr repeat(4, min-content);
grid-column-gap: $interiorMargin; grid-column-gap: $interiorMargin;
min-height: 34px;
padding: $interiorMargin $interiorMargin + 2; padding: $interiorMargin $interiorMargin + 2;
.l-shell__head__collapse-button { .l-shell__head__button {
color: $colorBtnMajorBg; color: $colorBtnMajorBg;
flex: 0 0 auto; flex: 0 0 auto;
font-size: 0.9em; font-size: 0.9em;
&--collapse {
&:before {
content: $glyph-icon-items-collapse;
}
}
&--expand {
&:before {
content: $glyph-icon-items-expand;
}
}
} }
&-section { &-section {
@ -271,14 +258,21 @@
} }
&__indicators { &__indicators {
// Style as multiline by default
display: flex;
flex-wrap: wrap; flex-wrap: wrap;
font-size: 11px; font-size: 11px;
min-height: 25px;
justify-content: flex-end; justify-content: flex-end;
.l-shell__head--expanded & { .l-shell__head--indicators-single-line & {
// Force elements to wrap down when width constrained flex-wrap: nowrap;
height: 24px; justify-content: flex-start; // Overflow detection doesn't work with flex-end.
overflow: hidden; overflow: hidden;
> *:first-child {
margin-left: auto; // Mimics justify-content: flex-end when in single line mode.
}
} }
} }

View File

@ -17,7 +17,7 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<template> <template>
<div class="l-shell__head-section l-shell__indicators"> <div ref="indicatorsContainer" class="l-shell__head-section l-shell__indicators">
<component <component
:is="indicator.value.vueComponent" :is="indicator.value.vueComponent"
v-for="indicator in sortedIndicators" v-for="indicator in sortedIndicators"
@ -28,9 +28,15 @@
</template> </template>
<script> <script>
import { shallowRef } from 'vue'; import { defineExpose, ref, shallowRef } from 'vue';
export default { export default {
inject: ['openmct'], inject: ['openmct'],
setup() {
const indicatorsContainer = ref(null);
defineExpose({ indicatorsContainer });
},
data() { data() {
return { return {
indicators: this.openmct.indicators.getIndicatorObjectsByPriority().map(shallowRef) indicators: this.openmct.indicators.getIndicatorObjectsByPriority().map(shallowRef)

View File

@ -1,5 +1,5 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government * Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved. * Administration. All rights reserved.
* *
@ -49,20 +49,19 @@ class VisibilityObserver {
* Constructs a VisibilityObserver instance to manage visibility-based requestAnimationFrame calls. * Constructs a VisibilityObserver instance to manage visibility-based requestAnimationFrame calls.
* *
* @param {HTMLElement} element - The DOM element to observe for visibility changes. * @param {HTMLElement} element - The DOM element to observe for visibility changes.
* @param {HTMLElement} rootContainer - The DOM element that is the root of the viewport.
* @throws {Error} If element is not provided. * @throws {Error} If element is not provided.
*/ */
constructor(element, rootContainer) { constructor(element) {
if (!element || !rootContainer) { if (!element) {
throw new Error(`VisibilityObserver must be created with an element and a rootContainer.`); throw new Error(`VisibilityObserver must be created with an element`);
} }
// set the id to some random 4 letters
this.id = Math.random().toString(36).substring(2, 6);
this.#element = element; this.#element = element;
this.isIntersecting = true; this.isIntersecting = true;
this.calledOnce = false;
const options = { this.#observer = new IntersectionObserver(this.#observerCallback);
root: rootContainer this.#observer.observe(this.#element);
};
this.#observer = new IntersectionObserver(this.#observerCallback, options);
this.lastUnfiredFunc = null; this.lastUnfiredFunc = null;
this.renderWhenVisible = this.renderWhenVisible.bind(this); this.renderWhenVisible = this.renderWhenVisible.bind(this);
} }