Merge branch 'master' into eval-source-maps

This commit is contained in:
David Tsay 2024-03-25 11:12:17 -07:00
commit cc58dbd5e7
32 changed files with 426 additions and 170 deletions

View File

@ -5,18 +5,19 @@ orbs:
executors: executors:
pw-focal-development: pw-focal-development:
docker: docker:
- image: mcr.microsoft.com/playwright:v1.39.0-focal - image: mcr.microsoft.com/playwright:v1.42.1-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
ubuntu: ubuntu:
machine: machine:
image: ubuntu-2204:current image: ubuntu-2204:current
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
@ -26,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)
@ -37,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
@ -101,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:
@ -158,7 +159,7 @@ jobs:
steps: steps:
- build_and_install: - build_and_install:
node-version: lts/hydrogen node-version: lts/hydrogen
- run: npx playwright@1.39.0 install #Necessary for bare ubuntu machine - run: npx playwright@1.42.1 install #Necessary for bare ubuntu machine
- run: | - run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs) export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
@ -220,14 +221,15 @@ jobs:
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps: steps:
- generate_and_store_version_and_filesystem_artifacts - generate_and_store_version_and_filesystem_artifacts
visual-a11y-tests: visual-a11y:
parameters: parameters:
suite: suite:
type: string # ci or full type: string # ci or full
executor: pw-focal-development executor: pw-focal-development
parallelism: 2
steps: steps:
- build_and_install: - build_and_install:
node-version: lts/hydrogen node-version: lts/iron
- run: npm run test:e2e:visual:<<parameters.suite>> - run: npm run test:e2e:visual:<<parameters.suite>>
- store_test_results: - store_test_results:
path: test-results/results.xml path: test-results/results.xml
@ -254,8 +256,8 @@ workflows:
name: e2e-stable name: e2e-stable
suite: stable suite: stable
- e2e-mobile - e2e-mobile
- visual-a11y-tests: - visual-a11y:
name: visual-a11y-test-ci name: visual-a11y-ci
suite: ci suite: ci
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
@ -274,13 +276,13 @@ workflows:
- e2e-mobile - e2e-mobile
- perf-test - perf-test
- mem-test - mem-test
- visual-a11y-tests: - visual-a11y:
name: visual-a11y-test-nightly name: visual-a11y-nightly
suite: full suite: full
- e2e-couchdb - e2e-couchdb
triggers: triggers:
- schedule: - schedule:
cron: "0 0 * * *" cron: '0 0 * * *'
filters: filters:
branches: branches:
only: only:

View File

@ -37,7 +37,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: npx playwright@1.39.0 install - run: npx playwright@1.42.1 install
- name: Start CouchDB Docker Container and Init with Setup Scripts - name: Start CouchDB Docker Container and Init with Setup Scripts
run: | run: |

View File

@ -30,7 +30,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-node- ${{ runner.os }}-node-
- run: npx playwright@1.39.0 install - run: npx playwright@1.42.1 install
- run: npm ci --no-audit --progress=false - run: npm ci --no-audit --progress=false
- name: Run E2E Tests (Repeated 10 Times) - name: Run E2E Tests (Repeated 10 Times)

View File

@ -28,7 +28,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-node- ${{ runner.os }}-node-
- run: npx playwright@1.39.0 install - run: npx playwright@1.42.1 install
- run: npm ci --no-audit --progress=false - run: npm ci --no-audit --progress=false
- run: npm run test:perf:localhost - run: npm run test:perf:localhost
- run: npm run test:perf:contract - run: npm run test:perf:contract

View File

@ -33,7 +33,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-node- ${{ runner.os }}-node-
- run: npx playwright@1.39.0 install - run: npx playwright@1.42.1 install
- run: npx playwright install chrome-beta - run: npx playwright install chrome-beta
- run: npm ci --no-audit --progress=false - run: npm ci --no-audit --progress=false
- run: npm run test:e2e:full -- --max-failures=40 - run: npm run test:e2e:full -- --max-failures=40

View File

@ -16,8 +16,6 @@ The [CodeQL GitHub Actions workflow](https://github.com/nasa/openmct/blob/master
CodeQL is run for every pull-request in GitHub Actions. CodeQL is run for every pull-request in GitHub Actions.
The project is also monitored by [LGTM](https://lgtm.com/projects/g/nasa/openmct/) and is available to public.
### ESLint ### ESLint
Static analysis is run for every push on the master branch and every pull request on all branches in Github Actions. Static analysis is run for every push on the master branch and every pull request on all branches in Github Actions.

View File

@ -392,7 +392,8 @@ async function setTimeConductorMode(page, isFixedTimespan = true) {
await page.getByRole('menuitem', { name: /Real-Time/ }).click(); await page.getByRole('menuitem', { name: /Real-Time/ }).click();
await page.waitForURL(/tc\.mode=local/); await page.waitForURL(/tc\.mode=local/);
} }
await page.getByLabel('Submit time offsets').or(page.getByLabel('Submit time bounds')).click(); //dismiss the time conductor popup
await page.getByLabel('Discard changes and close time popup').click();
} }
/** /**

View File

@ -292,6 +292,16 @@ test.describe('Basic Condition Set Use', () => {
await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible(); await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible(); await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible(); await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible();
await page.getByLabel('Plot').click();
await expect(
page.getByLabel('Plot Legend Collapsed').getByText('Test Condition Set')
).toBeVisible();
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByLabel('Telemetry Table').click();
await expect(page.getByRole('searchbox', { name: 'output filter input' })).toBeVisible();
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByLabel('Conditions View').click();
await expect(page.getByText('Current Output')).toBeVisible();
}); });
test('ConditionSet has correct outputs when telemetry is and is not available', async ({ test('ConditionSet has correct outputs when telemetry is and is not available', async ({
page page
@ -457,4 +467,11 @@ test.describe('Basic Condition Set Use', () => {
await page.goto(exampleTelemetry.url); await page.goto(exampleTelemetry.url);
}); });
test.fixme('Ensure condition sets work with telemetry like operator status', ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7484'
});
});
}); });

View File

@ -0,0 +1,64 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createDomainObjectWithDefaults,
setIndependentTimeConductorBounds
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
const FIXED_TIME =
'./#/browse/mine?tc.mode=fixed&tc.startBound=1693592063607&tc.endBound=1693593893607&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true';
test.describe('Datepicker operations', () => {
test.beforeEach(async ({ page }) => {
await page.goto(FIXED_TIME);
});
test('Verify that user can use the datepicker in the TC', async ({ page }) => {
await page.getByLabel('Time Conductor Mode').click();
// Click on the date picker that is left-most on the screen
await page.getByLabel('Global Time Conductor').locator('a').first().click();
await expect(page.getByRole('dialog')).toBeVisible();
// Click on the first cell
await page.getByText('27 239').click();
// Expect datepicker to close and time conductor date setting to be changed
await expect(page.getByRole('dialog')).toHaveCount(0);
});
test('Verify that user can use the datepicker in the ITC', async ({ page }) => {
const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });
await page.goto(createdTimeList.url, { waitUntil: 'domcontentloaded' });
await setIndependentTimeConductorBounds(page, {
start: '2024-11-12 19:11:11.000Z',
end: '2024-11-12 20:11:11.000Z'
});
// Open ITC
await page.getByLabel('Start bounds').nth(0).click();
// Click on the datepicker icon
await page.locator('form a').first().click();
await expect(page.getByRole('dialog')).toBeVisible();
// Click on the first cell
await page.getByText('7 342').click();
// Expect datepicker to close and time conductor date setting to be changed
await expect(page.getByRole('dialog')).toHaveCount(0);
});
});

View File

@ -72,11 +72,29 @@ test.describe('Visual - Planning', () => {
name: 'Plan Visual Test', name: 'Plan Visual Test',
json: examplePlanSmall2 json: examplePlanSmall2
}); });
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url); await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
await percySnapshot(page, `Plan View (theme: ${theme})`); await percySnapshot(page, `Plan View (theme: ${theme})`);
}); });
test('Resize Plan View @2p', async ({ browser, theme }) => {
// need to set viewport to null to allow for resizing
const newContext = await browser.newContext({
viewport: null
});
const newPage = await newContext.newPage();
await newPage.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
const plan = await createPlanFromJSON(newPage, {
name: 'Plan Visual Test',
json: examplePlanSmall2
});
await setBoundsToSpanAllActivities(newPage, examplePlanSmall2, plan.url);
// resize the window
await newPage.setViewportSize({ width: 800, height: 600 });
await percySnapshot(newPage, `Plan View resized (theme: ${theme})`);
});
test('Plan View w/ draft status', async ({ page, theme }) => { test('Plan View w/ draft status', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, { const plan = await createPlanFromJSON(page, {
name: 'Plan Visual Test (Draft)', name: 'Plan Visual Test (Draft)',

45
package-lock.json generated
View File

@ -14,14 +14,14 @@
"@braintree/sanitize-url": "6.0.4", "@braintree/sanitize-url": "6.0.4",
"@percy/cli": "1.27.4", "@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4", "@percy/playwright": "1.0.4",
"@playwright/test": "1.39.0", "@playwright/test": "1.42.1",
"@types/d3-axis": "3.0.6", "@types/d3-axis": "3.0.6",
"@types/d3-scale": "4.0.8", "@types/d3-scale": "4.0.8",
"@types/d3-selection": "3.0.10", "@types/d3-selection": "3.0.10",
"@types/d3-shape": "3.0.0", "@types/d3-shape": "3.0.0",
"@types/eventemitter3": "1.2.0", "@types/eventemitter3": "1.2.0",
"@types/jasmine": "5.1.2", "@types/jasmine": "5.1.2",
"@types/lodash": "4.14.192", "@types/lodash": "4.17.0",
"@vue/compiler-sfc": "3.4.3", "@vue/compiler-sfc": "3.4.3",
"babel-loader": "9.1.0", "babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1", "babel-plugin-istanbul": "6.1.1",
@ -1548,12 +1548,12 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.39.0", "version": "1.42.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz",
"integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"playwright": "1.39.0" "playwright": "1.42.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -1821,9 +1821,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/lodash": { "node_modules/@types/lodash": {
"version": "4.14.192", "version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.192.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
"integrity": "sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==", "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==",
"dev": true "dev": true
}, },
"node_modules/@types/mime": { "node_modules/@types/mime": {
@ -6027,9 +6027,9 @@
"dev": true "dev": true
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.5", "version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -9026,12 +9026,12 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.39.0", "version": "1.42.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz",
"integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"playwright-core": "1.39.0" "playwright-core": "1.42.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -9048,7 +9048,6 @@
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz",
"integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==",
"dev": true, "dev": true,
"peer": true,
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
}, },
@ -9070,18 +9069,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/playwright/node_modules/playwright-core": {
"version": "1.39.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz",
"integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/plotly.js-basic-dist-min": { "node_modules/plotly.js-basic-dist-min": {
"version": "2.29.1", "version": "2.29.1",
"resolved": "https://registry.npmjs.org/plotly.js-basic-dist-min/-/plotly.js-basic-dist-min-2.29.1.tgz", "resolved": "https://registry.npmjs.org/plotly.js-basic-dist-min/-/plotly.js-basic-dist-min-2.29.1.tgz",

View File

@ -10,14 +10,14 @@
"@braintree/sanitize-url": "6.0.4", "@braintree/sanitize-url": "6.0.4",
"@percy/cli": "1.27.4", "@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4", "@percy/playwright": "1.0.4",
"@playwright/test": "1.39.0", "@playwright/test": "1.42.1",
"@types/d3-axis": "3.0.6", "@types/d3-axis": "3.0.6",
"@types/d3-shape": "3.0.0", "@types/d3-shape": "3.0.0",
"@types/d3-scale": "4.0.8", "@types/d3-scale": "4.0.8",
"@types/d3-selection": "3.0.10", "@types/d3-selection": "3.0.10",
"@types/eventemitter3": "1.2.0", "@types/eventemitter3": "1.2.0",
"@types/jasmine": "5.1.2", "@types/jasmine": "5.1.2",
"@types/lodash": "4.14.192", "@types/lodash": "4.17.0",
"@vue/compiler-sfc": "3.4.3", "@vue/compiler-sfc": "3.4.3",
"babel-loader": "9.1.0", "babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1", "babel-plugin-istanbul": "6.1.1",

View File

@ -56,20 +56,38 @@ export default class ConditionManager extends EventEmitter {
); );
} }
subscribeToTelemetry(endpoint) { async requestLatestValue(endpoint) {
const id = this.openmct.objects.makeKeyString(endpoint.identifier); const options = {
if (this.subscriptions[id]) { size: 1,
console.log('subscription already exists'); strategy: 'latest'
};
const latestData = await this.openmct.telemetry.request(endpoint, options);
if (!latestData) {
throw new Error('Telemetry request failed by returning a falsy response');
}
if (latestData.length === 0) {
return;
}
this.telemetryReceived(endpoint, latestData[0]);
}
subscribeToTelemetry(endpoint) {
const telemetryKeyString = this.openmct.objects.makeKeyString(endpoint.identifier);
if (this.subscriptions[telemetryKeyString]) {
return; return;
} }
const metadata = this.openmct.telemetry.getMetadata(endpoint); const metadata = this.openmct.telemetry.getMetadata(endpoint);
this.telemetryObjects[id] = Object.assign({}, endpoint, { this.telemetryObjects[telemetryKeyString] = Object.assign({}, endpoint, {
telemetryMetaData: metadata ? metadata.valueMetadatas : [] telemetryMetaData: metadata ? metadata.valueMetadatas : []
}); });
this.subscriptions[id] = this.openmct.telemetry.subscribe(
// get latest telemetry value (in case subscription is cached and no new data is coming in)
this.requestLatestValue(endpoint);
this.subscriptions[telemetryKeyString] = this.openmct.telemetry.subscribe(
endpoint, endpoint,
this.telemetryReceived.bind(this, endpoint) this.telemetryReceived.bind(this, endpoint)
); );
@ -91,7 +109,7 @@ export default class ConditionManager extends EventEmitter {
//force re-computation of condition set result as we might be in a state where //force re-computation of condition set result as we might be in a state where
// there is no telemetry datum coming in for a while or at all. // there is no telemetry datum coming in for a while or at all.
let latestTimestamp = getLatestTimestamp( const latestTimestamp = getLatestTimestamp(
{}, {},
{}, {},
this.timeSystems, this.timeSystems,
@ -334,57 +352,54 @@ export default class ConditionManager extends EventEmitter {
return currentCondition; return currentCondition;
} }
requestLADConditionSetOutput(options) { async requestLADConditionSetOutput(options) {
if (!this.conditions.length) { if (!this.conditions.length) {
return Promise.resolve([]); return [];
} }
return this.compositionLoad.then(() => { await this.compositionLoad;
let latestTimestamp;
let conditionResults = {};
let nextLegOptions = { ...options };
delete nextLegOptions.onPartialResponse;
const conditionRequests = this.conditions.map((condition) => let latestTimestamp;
condition.requestLADConditionResult(nextLegOptions) let conditionResults = {};
let nextLegOptions = { ...options };
delete nextLegOptions.onPartialResponse;
const results = await Promise.all(
this.conditions.map((condition) => condition.requestLADConditionResult(nextLegOptions))
);
results.forEach((resultObj) => {
const {
id,
data,
data: { result }
} = resultObj;
if (this.findConditionById(id)) {
conditionResults[id] = Boolean(result);
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
); );
return Promise.all(conditionRequests).then((results) => {
results.forEach((resultObj) => {
const {
id,
data,
data: { result }
} = resultObj;
if (this.findConditionById(id)) {
conditionResults[id] = Boolean(result);
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
);
});
if (!Object.values(latestTimestamp).some((timeSystem) => timeSystem)) {
return [];
}
const currentCondition = this.getCurrentConditionLAD(conditionResults);
const currentOutput = Object.assign(
{
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id
},
latestTimestamp
);
return [currentOutput];
});
}); });
if (!Object.values(latestTimestamp).some((timeSystem) => timeSystem)) {
return [];
}
const currentCondition = this.getCurrentConditionLAD(conditionResults);
const currentOutput = {
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id,
...latestTimestamp
};
return [currentOutput];
} }
isTelemetryUsed(endpoint) { isTelemetryUsed(endpoint) {
@ -409,7 +424,7 @@ export default class ConditionManager extends EventEmitter {
} }
const normalizedDatum = this.createNormalizedDatum(datum, endpoint); const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
const timeSystemKey = this.openmct.time.timeSystem().key; const timeSystemKey = this.openmct.time.getTimeSystem().key;
let timestamp = {}; let timestamp = {};
const currentTimestamp = normalizedDatum[timeSystemKey]; const currentTimestamp = normalizedDatum[timeSystemKey];
timestamp[timeSystemKey] = currentTimestamp; timestamp[timeSystemKey] = currentTimestamp;

View File

@ -40,12 +40,10 @@ export default class ConditionSetTelemetryProvider {
return domainObject.type === 'conditionSet'; return domainObject.type === 'conditionSet';
} }
request(domainObject, options) { async request(domainObject, options) {
let conditionManager = this.getConditionManager(domainObject); let conditionManager = this.getConditionManager(domainObject);
let latestOutput = await conditionManager.requestLADConditionSetOutput(options);
return conditionManager.requestLADConditionSetOutput(options).then((latestOutput) => { return latestOutput;
return latestOutput;
});
} }
subscribe(domainObject, callback) { subscribe(domainObject, callback) {

View File

@ -66,7 +66,8 @@ describe('the plugin', function () {
format: 'utc', format: 'utc',
hints: { hints: {
domain: 1 domain: 1
} },
source: 'utc'
}, },
{ {
key: 'testSource', key: 'testSource',
@ -720,6 +721,23 @@ describe('the plugin', function () {
}); });
it('should evaluate as old when telemetry is not received in the allotted time', (done) => { it('should evaluate as old when telemetry is not received in the allotted time', (done) => {
openmct.telemetry = jasmine.createSpyObj('telemetry', [
'subscribe',
'getMetadata',
'request',
'getValueFormatter',
'abortAllRequests'
]);
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: []
});
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
openmct.telemetry.getValueFormatter.and.returnValue({
parse: function (value) {
return value;
}
});
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener); conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = { conditionMgr.telemetryObjects = {
@ -741,6 +759,20 @@ describe('the plugin', function () {
}); });
it('should not evaluate as old when telemetry is received in the allotted time', (done) => { it('should not evaluate as old when telemetry is received in the allotted time', (done) => {
openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata');
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: testTelemetryObject.telemetry.values
});
const testDatum = {
'some-key2': '',
utc: 1,
testSource: '',
'some-key': null,
id: 'test-object'
};
openmct.telemetry.request = jasmine.createSpy('request');
openmct.telemetry.request.and.returnValue(Promise.resolve([testDatum]));
const date = 1; const date = 1;
conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input = conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input =
['0.4']; ['0.4'];
@ -750,9 +782,7 @@ describe('the plugin', function () {
'test-object': testTelemetryObject 'test-object': testTelemetryObject
}; };
conditionMgr.updateConditionTelemetryObjects(); conditionMgr.updateConditionTelemetryObjects();
conditionMgr.telemetryReceived(testTelemetryObject, { conditionMgr.telemetryReceived(testTelemetryObject, testDatum);
utc: date
});
setTimeout(() => { setTimeout(() => {
expect(mockListener).toHaveBeenCalledWith({ expect(mockListener).toHaveBeenCalledWith({
output: 'Default', output: 'Default',
@ -868,6 +898,12 @@ describe('the plugin', function () {
it('should stop evaluating conditions when a condition evaluates to true', () => { it('should stop evaluating conditions when a condition evaluates to true', () => {
const date = Date.now(); const date = Date.now();
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct); let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata');
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: []
});
conditionMgr.on('conditionSetResultUpdated', mockListener); conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = { conditionMgr.telemetryObjects = {
'test-object': testTelemetryObject 'test-object': testTelemetryObject

View File

@ -150,16 +150,15 @@ export default class ImportAsJSONAction {
* @param {string} namespace * @param {string} namespace
* @returns {object} * @returns {object}
*/ */
_generateNewIdentifiers(tree, namespace) { _generateNewIdentifiers(tree, newNamespace) {
// For each domain object in the file, generate new ID, replace in tree // For each domain object in the file, generate new ID, replace in tree
Object.keys(tree.openmct).forEach((domainObjectId) => { Object.keys(tree.openmct).forEach((domainObjectId) => {
const newId = {
namespace,
key: uuid()
};
const oldId = parseKeyString(domainObjectId); const oldId = parseKeyString(domainObjectId);
const newId = {
namespace: newNamespace,
key: uuid()
};
tree = this._rewriteId(oldId, newId, tree); tree = this._rewriteId(oldId, newId, tree);
}, this); }, this);
@ -228,22 +227,32 @@ export default class ImportAsJSONAction {
_rewriteId(oldId, newId, tree) { _rewriteId(oldId, newId, tree) {
let newIdKeyString = this.openmct.objects.makeKeyString(newId); let newIdKeyString = this.openmct.objects.makeKeyString(newId);
let oldIdKeyString = this.openmct.objects.makeKeyString(oldId); let oldIdKeyString = this.openmct.objects.makeKeyString(oldId);
tree = JSON.stringify(tree).replace(new RegExp(oldIdKeyString, 'g'), newIdKeyString); const newTreeString = JSON.stringify(tree).replace(
new RegExp(oldIdKeyString, 'g'),
return JSON.parse(tree, (key, value) => { newIdKeyString
);
const newTree = JSON.parse(newTreeString, (key, value) => {
if ( if (
value !== undefined && value !== undefined &&
value !== null && value !== null &&
Object.prototype.hasOwnProperty.call(value, 'key') && Object.prototype.hasOwnProperty.call(value, 'key') &&
Object.prototype.hasOwnProperty.call(value, 'namespace') && Object.prototype.hasOwnProperty.call(value, 'namespace')
value.key === oldId.key &&
value.namespace === oldId.namespace
) { ) {
return newId; // first check if key is messed up from regex and contains a colon
} else { // if it does, repair it
return value; if (value.key.includes(':')) {
const splitKey = value.key.split(':');
value.key = splitKey[1];
value.namespace = splitKey[0];
}
// now check if we need to replace the id
if (value.key === oldId.key && value.namespace === oldId.namespace) {
return newId;
}
} }
return value;
}); });
return newTree;
} }
/** /**
* @private * @private

View File

@ -135,11 +135,75 @@ describe('The import JSON action', function () {
selectFile: { selectFile: {
name: 'imported object', name: 'imported object',
// eslint-disable-next-line prettier/prettier // eslint-disable-next-line prettier/prettier
body: "{\"openmct\":{\"c28d230d-e909-4a3e-9840-d9ef469dda70\":{\"identifier\":{\"key\":\"c28d230d-e909-4a3e-9840-d9ef469dda70\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[],\"configuration\":{\"series\":[]},\"modified\":1695837546833,\"location\":\"mine\",\"created\":1695837546833,\"persisted\":1695837546833,\"__proto__\":{\"toString\":\"foobar\"}}},\"rootId\":\"c28d230d-e909-4a3e-9840-d9ef469dda70\"}" body: '{"openmct":{"c28d230d-e909-4a3e-9840-d9ef469dda70":{"identifier":{"key":"c28d230d-e909-4a3e-9840-d9ef469dda70","namespace":""},"name":"Unnamed Overlay Plot","type":"telemetry.plot.overlay","composition":[],"configuration":{"series":[]},"modified":1695837546833,"location":"mine","created":1695837546833,"persisted":1695837546833,"__proto__":{"toString":"foobar"}}},"rootId":"c28d230d-e909-4a3e-9840-d9ef469dda70"}'
} }
}; };
return Promise.resolve(pollutedResponse); return Promise.resolve(pollutedResponse);
} }
}); });
it('preserves the integrity of the namespace and key during import', async () => {
const incomingObject = {
openmct: {
'7323f02a-06ac-438d-bd58-6d6e33b8741e': {
name: 'Some Folder',
type: 'folder',
composition: [
{
key: '9f6c2d21-5ec8-434c-9fe8-31614ae6d7e6',
namespace: ''
}
],
modified: 1710843256162,
location: 'mine',
created: 1710843243471,
persisted: 1710843256162,
identifier: {
namespace: '',
key: '7323f02a-06ac-438d-bd58-6d6e33b8741e'
}
},
'9f6c2d21-5ec8-434c-9fe8-31614ae6d7e6': {
name: 'Some Clock',
type: 'clock',
configuration: {
baseFormat: 'YYYY/MM/DD hh:mm:ss',
use24: 'clock12',
timezone: 'UTC'
},
modified: 1710843256152,
location: '7323f02a-06ac-438d-bd58-6d6e33b8741e',
created: 1710843256152,
persisted: 1710843256152,
identifier: {
namespace: '',
key: '9f6c2d21-5ec8-434c-9fe8-31614ae6d7e6'
}
}
},
rootId: '7323f02a-06ac-438d-bd58-6d6e33b8741e'
};
const targetDomainObject = {
identifier: {
namespace: 'starJones',
key: '84438cda-a071-48d1-b9bf-d77bd53e59ba'
},
type: 'folder'
};
spyOn(openmct.objects, 'save').and.callFake((model) => Promise.resolve(model));
try {
await importFromJSONAction.onSave(targetDomainObject, {
selectFile: { body: JSON.stringify(incomingObject) }
});
for (const callArgs of openmct.objects.save.calls.allArgs()) {
const savedObject = callArgs[0]; // Assuming the first argument is the object being saved.
expect(savedObject.identifier.key.includes(':')).toBeFalse(); // Ensure no colon in the key.
expect(savedObject.identifier.namespace).toBe(targetDomainObject.identifier.namespace);
}
} catch (error) {
fail(error);
}
});
}); });

View File

@ -169,6 +169,8 @@ describe('Notebook plugin:', () => {
openmct.editor = {}; openmct.editor = {};
openmct.editor.isEditing = () => false; openmct.editor.isEditing = () => false;
openmct.editor.on = () => {};
openmct.editor.off = () => {};
const applicableViews = openmct.objectViews.get(notebookViewObject, [notebookViewObject]); const applicableViews = openmct.objectViews.get(notebookViewObject, [notebookViewObject]);
notebookViewProvider = applicableViews.find( notebookViewProvider = applicableViews.find(

View File

@ -25,7 +25,7 @@ import mount from 'utils/mount';
import TableConfigurationComponent from './components/TableConfiguration.vue'; import TableConfigurationComponent from './components/TableConfiguration.vue';
import TelemetryTableConfiguration from './TelemetryTableConfiguration.js'; import TelemetryTableConfiguration from './TelemetryTableConfiguration.js';
export default function TableConfigurationViewProvider(openmct) { export default function TableConfigurationViewProvider(openmct, options) {
return { return {
key: 'table-configuration', key: 'table-configuration',
name: 'Config', name: 'Config',
@ -45,7 +45,7 @@ export default function TableConfigurationViewProvider(openmct) {
return { return {
show: function (element) { show: function (element) {
tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct); tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct, options);
const { destroy } = mount( const { destroy } = mount(
{ {
el: element, el: element,

View File

@ -32,14 +32,14 @@ import TelemetryTableRow from './TelemetryTableRow.js';
import TelemetryTableUnitColumn from './TelemetryTableUnitColumn.js'; import TelemetryTableUnitColumn from './TelemetryTableUnitColumn.js';
export default class TelemetryTable extends EventEmitter { export default class TelemetryTable extends EventEmitter {
constructor(domainObject, openmct) { constructor(domainObject, openmct, options) {
super(); super();
this.domainObject = domainObject; this.domainObject = domainObject;
this.openmct = openmct; this.openmct = openmct;
this.tableComposition = undefined; this.tableComposition = undefined;
this.datumCache = []; this.datumCache = [];
this.configuration = new TelemetryTableConfiguration(domainObject, openmct); this.configuration = new TelemetryTableConfiguration(domainObject, openmct, options);
this.telemetryMode = this.configuration.getTelemetryMode(); this.telemetryMode = this.configuration.getTelemetryMode();
this.rowLimit = this.configuration.getRowLimit(); this.rowLimit = this.configuration.getRowLimit();
this.paused = false; this.paused = false;

View File

@ -24,11 +24,12 @@ import EventEmitter from 'EventEmitter';
import _ from 'lodash'; import _ from 'lodash';
export default class TelemetryTableConfiguration extends EventEmitter { export default class TelemetryTableConfiguration extends EventEmitter {
constructor(domainObject, openmct) { constructor(domainObject, openmct, options) {
super(); super();
this.domainObject = domainObject; this.domainObject = domainObject;
this.openmct = openmct; this.openmct = openmct;
this.defaultOptions = options;
this.columns = {}; this.columns = {};
this.removeColumnsForObject = this.removeColumnsForObject.bind(this); this.removeColumnsForObject = this.removeColumnsForObject.bind(this);
@ -48,10 +49,12 @@ export default class TelemetryTableConfiguration extends EventEmitter {
configuration.columnOrder = configuration.columnOrder || []; configuration.columnOrder = configuration.columnOrder || [];
configuration.cellFormat = configuration.cellFormat || {}; configuration.cellFormat = configuration.cellFormat || {};
configuration.autosize = configuration.autosize === undefined ? true : configuration.autosize; configuration.autosize = configuration.autosize === undefined ? true : configuration.autosize;
// anything that doesn't have a telemetryMode existed before the change and should stay as it was for consistency // anything that doesn't have a telemetryMode existed before the change and should
configuration.telemetryMode = configuration.telemetryMode ?? 'unlimited'; // take the properties of any passed in defaults or the defaults from the plugin
configuration.persistModeChange = configuration.persistModeChange ?? true; configuration.telemetryMode = configuration.telemetryMode ?? this.defaultOptions.telemetryMode;
configuration.rowLimit = configuration.rowLimit ?? 50; configuration.persistModeChange =
configuration.persistModeChange ?? this.defaultOptions.persistModeChange;
configuration.rowLimit = configuration.rowLimit ?? this.defaultOptions.rowLimit;
return configuration; return configuration;
} }

View File

@ -20,8 +20,8 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
export default function getTelemetryTableType(options = {}) { export default function getTelemetryTableType(options) {
const { telemetryMode = 'performance', persistModeChange = true, rowLimit = 50 } = options; let { telemetryMode, persistModeChange, rowLimit } = options;
return { return {
name: 'Telemetry Table', name: 'Telemetry Table',

View File

@ -33,7 +33,7 @@ export default class TelemetryTableView {
this.component = null; this.component = null;
Object.defineProperty(this, 'table', { Object.defineProperty(this, 'table', {
value: new TelemetryTable(domainObject, openmct), value: new TelemetryTable(domainObject, openmct, options),
enumerable: false, enumerable: false,
configurable: false configurable: false
}); });

View File

@ -398,15 +398,17 @@ export default {
totalNumberOfRows: 0, totalNumberOfRows: 0,
rowContext: {}, rowContext: {},
telemetryMode: configuration.telemetryMode, telemetryMode: configuration.telemetryMode,
rowLimit: configuration.rowLimit,
persistModeChange: configuration.persistModeChange, persistModeChange: configuration.persistModeChange,
afterLoadActions: [] afterLoadActions: [],
existingConfiguration: configuration
}; };
}, },
computed: { computed: {
dropTargetStyle() { dropTargetStyle() {
return { return {
top: this.$refs.headersTable.offsetTop + 'px', top: this.$refs.headersHolderEl.offsetTop + 'px',
height: this.totalHeight + this.$refs.headersTable.offsetHeight + 'px', height: this.totalHeight + this.$refs.headersHolderEl.offsetHeight + 'px',
left: this.dropOffsetLeft && this.dropOffsetLeft + 'px' left: this.dropOffsetLeft && this.dropOffsetLeft + 'px'
}; };
}, },
@ -595,23 +597,35 @@ export default {
}, },
handleConfigurationChanges(changes) { handleConfigurationChanges(changes) {
const { rowLimit, telemetryMode, persistModeChange } = changes; const { rowLimit, telemetryMode, persistModeChange } = changes;
const telemetryModeChanged = this.existingConfiguration.telemetryMode !== telemetryMode;
let rowLimitChanged = false;
this.persistModeChange = persistModeChange; this.persistModeChange = persistModeChange;
// both rowLimit changes and telemetryMode changes
// require a re-request of telemetry
if (this.rowLimit !== rowLimit) { if (this.rowLimit !== rowLimit) {
rowLimitChanged = true;
this.rowLimit = rowLimit; this.rowLimit = rowLimit;
this.table.updateRowLimit(rowLimit); this.table.updateRowLimit(rowLimit);
if (this.telemetryMode !== telemetryMode) {
// need to clear and resubscribe, if different, handled below
this.table.clearAndResubscribe();
}
} }
if (this.telemetryMode !== telemetryMode) { // check for telemetry mode change, because you could technically have persist mode changes
// set to false, which could create a state where the configuration saved telemetry mode is
// different from the currently set telemetry mode
if (telemetryModeChanged && this.telemetryMode !== telemetryMode) {
this.telemetryMode = telemetryMode; this.telemetryMode = telemetryMode;
// this method also re-requests telemetry
this.table.updateTelemetryMode(telemetryMode); this.table.updateTelemetryMode(telemetryMode);
} }
if (rowLimitChanged && !telemetryModeChanged) {
this.table.clearAndResubscribe();
}
this.existingConfiguration = changes;
}, },
updateVisibleRows() { updateVisibleRows() {
if (!this.updatingView) { if (!this.updatingView) {

View File

@ -25,10 +25,12 @@ 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(options) { export default function plugin(
options = { telemetryMode: '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));
openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct)); openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct, options));
openmct.types.addType('table', getTelemetryTableType(options)); openmct.types.addType('table', getTelemetryTableType(options));
openmct.composition.addPolicy((parent, child) => { openmct.composition.addPolicy((parent, child) => {
if (parent.type === 'table') { if (parent.type === 'table') {

View File

@ -195,7 +195,10 @@ describe('the plugin', () => {
utc: false, utc: false,
'some-key': false, 'some-key': false,
'some-other-key': false 'some-other-key': false
} },
persistModeChange: true,
rowLimit: 50,
telemetryMode: 'performance'
} }
}; };
const testTelemetry = [ const testTelemetry = [

View File

@ -29,7 +29,7 @@
}" }"
> >
<a class="c-icon-button icon-calendar" @click="toggle"></a> <a class="c-icon-button icon-calendar" @click="toggle"></a>
<div v-if="open" class="c-menu c-menu--mobile-modal c-datetime-picker"> <div v-if="open" role="dialog" class="c-menu c-menu--mobile-modal c-datetime-picker">
<div class="c-datetime-picker__close-button"> <div class="c-datetime-picker__close-button">
<button class="c-click-icon icon-x-in-circle" @click="toggle"></button> <button class="c-click-icon icon-x-in-circle" @click="toggle"></button>
</div> </div>

View File

@ -21,7 +21,7 @@
/> />
<date-picker <date-picker
v-if="isUTCBased" v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left" class="c-ctrl-wrapper--menus-right"
:default-date-time="formattedBounds.start" :default-date-time="formattedBounds.start"
:formatter="timeFormatter" :formatter="timeFormatter"
@date-selected="startDateSelected" @date-selected="startDateSelected"
@ -87,7 +87,7 @@
></button> ></button>
<button <button
class="c-button icon-x" class="c-button icon-x"
aria-label="Discard time bounds" aria-label="Discard changes and close time popup"
@click.prevent="hide" @click.prevent="hide"
></button> ></button>
</div> </div>

View File

@ -132,7 +132,7 @@
></button> ></button>
<button <button
class="c-button icon-x" class="c-button icon-x"
aria-label="Discard time offsets" aria-label="Discard changes and close time popup"
@click.prevent="hide" @click.prevent="hide"
></button> ></button>
</div> </div>

View File

@ -454,6 +454,12 @@
color: $colorTimeRealtimeFg; color: $colorTimeRealtimeFg;
} }
} }
.c-ctrl-wrapper--menus-up{ // A bit hacky, but we are rewriting the CSS class here for ITC such that the calendar opens at the bottom to avoid cutoff
.c-menu {
top: auto;
bottom: revert !important;
};
}
} }
} }

View File

@ -711,6 +711,20 @@
} }
} }
&[class*='--menus-down'] {
.c-menu {
top: auto;
bottom: 100%;
}
}
&[class*='--menus-right'] {
.c-menu {
left: 0;
right: auto;
}
}
&[class*='--menus-left'], &[class*='--menus-left'],
&[class*='menus-to-left'] { &[class*='menus-to-left'] {
.c-menu { .c-menu {

View File

@ -77,12 +77,13 @@ export default {
}, },
setup() { setup() {
const axisHolder = ref(null); const axisHolder = ref(null);
const { size, startObserving } = useResizeObserver(); const { size: containerSize, startObserving } = useResizeObserver();
onMounted(() => { onMounted(() => {
startObserving(axisHolder.value); startObserving(axisHolder.value);
}); });
return { return {
containerSize: size axisHolder,
containerSize
}; };
}, },
watch: { watch: {
@ -95,8 +96,11 @@ export default {
contentHeight() { contentHeight() {
this.updateNowMarker(); this.updateNowMarker();
}, },
containerSize() { containerSize: {
this.resize(); handler() {
this.resize();
},
deep: true
} }
}, },
mounted() { mounted() {
@ -104,7 +108,7 @@ export default {
this.useSVG = true; this.useSVG = true;
} }
this.container = select(this.$refs.axisHolder); this.container = select(this.axisHolder);
this.svgElement = this.container.append('svg:svg'); this.svgElement = this.container.append('svg:svg');
// draw x axis with labels. CSS is used to position them. // draw x axis with labels. CSS is used to position them.
this.axisElement = this.svgElement this.axisElement = this.svgElement
@ -122,7 +126,7 @@ export default {
}, },
methods: { methods: {
resize() { resize() {
if (this.$refs.axisHolder.clientWidth !== this.width) { if (this.axisHolder.clientWidth !== this.width) {
this.setDimensions(); this.setDimensions();
this.drawAxis(this.bounds, this.timeSystem); this.drawAxis(this.bounds, this.timeSystem);
this.updateNowMarker(); this.updateNowMarker();
@ -139,11 +143,10 @@ export default {
} }
}, },
setDimensions() { setDimensions() {
const axisHolder = this.$refs.axisHolder; this.width = this.axisHolder.clientWidth;
this.width = axisHolder.clientWidth;
this.offsetWidth = this.width - this.offset; this.offsetWidth = this.width - this.offset;
this.height = Math.round(axisHolder.getBoundingClientRect().height); this.height = Math.round(this.axisHolder.getBoundingClientRect().height);
if (this.useSVG) { if (this.useSVG) {
this.svgElement.attr('width', this.width); this.svgElement.attr('width', this.width);