Compare commits

..

16 Commits

Author SHA1 Message Date
Andrew Henry
a3560352cd Batch save requests to minimize the number of requests required to import a JSON file 2023-05-05 15:06:59 -07:00
John Hill
f5eacc504b [Couchdb] Update couchdb init script and bump version to latest (#6643)
* chatty

* bad linebreak

* bump to 3.3.2

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-05-05 19:24:54 +00:00
Marcelo Arias
26fa1653e3 Synchronize versions of common devDependencies with openmct-yamcs (#6627)
* Update @babel/eslint-parser, eslint, webpack, webpack-cli versions

* Increase eventemitter3 version from 1.2.0 to 4.0.7

* Downgrade eventemitter3 to 1.2.0

---------

Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-05-05 17:27:01 +00:00
Jamie V
b7c68f715b [LAD Table][Browse Bar] Visual test and title attributes for actions (#6640) 2023-05-03 18:11:01 -07:00
Jesse Mazzella
549a579bf3 fix: remove pr:e2e:couchdb label on run completion (#6628)
* fix: trigger e2e-couchdb run on sync

* fix: remove `e2e-couchdb` label if present after run

* fix: remove `synchronize` trigger

- this is intended behavior

* refactor: update GHA to use octokit action

* docs: add note about GHA warnings

* fix: remove `pr:e2e` label after run

* fix: use github-script
2023-05-03 10:24:41 -07:00
Jamie V
fe677fa359 [LAD Tables] Persist view modified configuration (#6637)
* chore: bump version to `2.2.2` (#6615)

* persisting lad configuration in the view when it changes

---------

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2023-05-02 16:19:23 -07:00
dependabot[bot]
1bbc3789ec chore(deps-dev): bump sass from 1.62.0 to 1.62.1 (#6630) 2023-05-02 12:20:00 -07:00
dependabot[bot]
636849885b chore(deps-dev): bump webpack from 5.80.0 to 5.81.0 (#6634)
Bumps [webpack](https://github.com/webpack/webpack) from 5.80.0 to 5.81.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.80.0...v5.81.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-02 09:02:46 -07:00
David Tsay
6f2b20eee9 Retain styling on condition widgets when adding or removing a url (#6625)
* fix spelling error
* apply changes after dynamic component updates
* remove * listener
* react to url change
* es6 mode
* fix html structure
* Closes #6614
- CSS fixes for revised widget approach.
* include url prop for vue component reactivity
* disable a tag overriding font color
* provide a reactive object for component reactivity

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
2023-04-28 20:17:14 +00:00
dependabot[bot]
e38821cc1f chore(deps-dev): bump @percy/cli from 1.23.0 to 1.24.0 (#6618)
Bumps [@percy/cli](https://github.com/percy/cli/tree/HEAD/packages/cli) from 1.23.0 to 1.24.0.
- [Release notes](https://github.com/percy/cli/releases)
- [Commits](https://github.com/percy/cli/commits/v1.24.0/packages/cli)

---
updated-dependencies:
- dependency-name: "@percy/cli"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-25 16:26:37 -07:00
dependabot[bot]
4345d216f7 chore(deps-dev): bump karma-chrome-launcher from 3.1.1 to 3.2.0 (#6619)
Bumps [karma-chrome-launcher](https://github.com/karma-runner/karma-chrome-launcher) from 3.1.1 to 3.2.0.
- [Release notes](https://github.com/karma-runner/karma-chrome-launcher/releases)
- [Changelog](https://github.com/karma-runner/karma-chrome-launcher/blob/master/CHANGELOG.md)
- [Commits](https://github.com/karma-runner/karma-chrome-launcher/compare/v3.1.1...v3.2.0)

---
updated-dependencies:
- dependency-name: karma-chrome-launcher
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-25 22:43:41 +00:00
dependabot[bot]
84a12c7833 chore(deps-dev): bump karma from 6.3.20 to 6.4.2 (#6616)
Bumps [karma](https://github.com/karma-runner/karma) from 6.3.20 to 6.4.2.
- [Release notes](https://github.com/karma-runner/karma/releases)
- [Changelog](https://github.com/karma-runner/karma/blob/master/CHANGELOG.md)
- [Commits](https://github.com/karma-runner/karma/compare/v6.3.20...v6.4.2)

---
updated-dependencies:
- dependency-name: karma
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-25 22:30:58 +00:00
dependabot[bot]
ad8445114f chore(deps-dev): bump webpack-cli from 5.0.0 to 5.0.2 (#6622)
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 5.0.0 to 5.0.2.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@5.0.0...webpack-cli@5.0.2)

---
updated-dependencies:
- dependency-name: webpack-cli
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-25 15:10:04 -07:00
dependabot[bot]
bcd50dfa35 chore(deps-dev): bump webpack from 5.79.0 to 5.80.0 (#6620)
Bumps [webpack](https://github.com/webpack/webpack) from 5.79.0 to 5.80.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.79.0...v5.80.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-25 14:40:29 -07:00
dependabot[bot]
a798ddf05e chore(deps-dev): bump eslint from 8.37.0 to 8.39.0 (#6617)
Bumps [eslint](https://github.com/eslint/eslint) from 8.37.0 to 8.39.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.37.0...v8.39.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-25 09:29:54 -07:00
Jesse Mazzella
7af7e68779 refactor: add appActions and stabilize overlayPlot and plotRendering e2e test suites (#6612)
* test: add appActions, stabilize overlayPlot test

- Adds `waitForPlotsToRender`, a function that waits for all active `.plot` elements on the page to load and draw their series data
- Adds `getCanvasPixels`, a function that takes a canvas selector and retrieves an array of canvas pixel data.
- Modifies `getCanvasPixels` to use `page.evaluateHandle()` so that the canvas handle lifetime exists throughout the test (this was causing flakiness before)

* test: refactor and stabilize `plotRendering` tests

* test: remove redundant test suite

* test: stabilize plot legend color swatch test

* docs: mention `waitForPlotsToLoad()` in e2e docs

* refactor: have getCanvasPixels return actual rgba values

* docs: fix typo

* test: use appAction and fix reload wait condition

* docs: add additional context for `waitForPlotsToRender()`

* refactor: one-liner

* docs: tidy up docs
2023-04-20 14:36:58 -07:00
55 changed files with 1119 additions and 2726 deletions

View File

@@ -41,3 +41,20 @@ jobs:
uses: actions/upload-artifact@v3
with:
path: html-test-results
- name: Remove pr:e2e:couchdb label (if present)
if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e:couchdb') }}
uses: actions/github-script@v6
with:
script: |
const { owner, repo, number } = context.issue;
const labelToRemove = 'pr:e2e:couchdb';
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: number,
name: labelToRemove
});
} catch (error) {
core.warning(`Failed to remove 'pr:e2e:couchdb' label: ${error.message}`);
}

View File

@@ -66,3 +66,20 @@ jobs:
repo: "openmct",
body: 'Failure ❌ ! Build artifacts are here: https://github.com/nasa/openmct/actions/runs/' + context.runId
})
- name: Remove pr:e2e label (if present)
if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e') }}
uses: actions/github-script@v6
with:
script: |
const { owner, repo, number } = context.issue;
const labelToRemove = 'pr:e2e';
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: number,
name: labelToRemove
});
} catch (error) {
core.warning(`Failed to remove 'pr:e2e' label: ${error.message}`);
}

View File

@@ -139,16 +139,18 @@ These tests are expected to become blocking and gating with assertions as we ext
Our file structure follows the type of type of testing being excercised at the e2e layer and files containing test suites which matcher application behavior or our `src` and `example` layout. This area is not well refined as we figure out what works best for closed source and downstream projects. This may change altogether if we move `e2e` to it's own npm package.
- `./helper` - contains helper functions or scripts which are leveraged directly within the testsuites. i.e. non-default plugin scripts injected into DOM
- `./test-data` - contains test data which is leveraged or generated in the functional, performance, or visual test suites. i.e. localStorage data
- `./tests/functional` - the bulk of the tests are contained within this folder to verify the functionality of open mct
- `./tests/functional/example/` - tests which specifically verify the example plugins
- `./tests/functional/plugins/` - tests which loosely test each plugin. This folder is the most likely to change. Note: some @snapshot tests are still contained within this structure
- `./tests/framework/` - tests which verify that our testframework functionality and assumptions will continue to work based on further refactoring or playwright version changes
- `./tests/performance/` - performance tests
- `./tests/visual/` - Visual tests
- `./appActions.js` - Contains common fixtures which can be leveraged by testcase authors to quickly move through the application when writing new tests.
- `./baseFixture.js` - Contains base fixtures which only extend default `@playwright/test` functionality. The goal is to remove these fixtures as native Playwright APIs improve.
|File Path|Description|
|:-:|-|
|`./helper` | Contains helper functions or scripts which are leveraged directly within the test suites (e.g.: non-default plugin scripts injected into the DOM)|
|`./test-data` | Contains test data which is leveraged or generated in the functional, performance, or visual test suites (e.g.: localStorage data).|
|`./tests/functional` | The bulk of the tests are contained within this folder to verify the functionality of Open MCT.|
|`./tests/functional/example/` | Tests which specifically verify the example plugins (e.g.: Sine Wave Generator).|
|`./tests/functional/plugins/` | Tests which loosely test each plugin. This folder is the most likely to change. Note: some `@snapshot` tests are still contained within this structure.|
|`./tests/framework/` | Tests which verify that our testing framework's functionality and assumptions will continue to work based on further refactoring or Playwright version changes (e.g.: verifying custom fixtures and appActions).|
|`./tests/performance/` | Performance tests.|
|`./tests/visual/` | Visual tests.|
|`./appActions.js` | Contains common methods which can be leveraged by test case authors to quickly move through the application when writing new tests.|
|`./baseFixture.js` | Contains base fixtures which only extend default `@playwright/test` functionality. The expectation is that these fixtures will be removed as the native Playwright API improves|
Our functional tests end in `*.e2e.spec.js`, visual tests in `*.visual.spec.js` and performance tests in `*.perf.spec.js`.
@@ -158,10 +160,12 @@ Where possible, we try to run Open MCT without modification or configuration cha
Open MCT is leveraging the [config file](https://playwright.dev/docs/test-configuration) pattern to describe the capabilities of Open MCT e2e _where_ it's run
- `./playwright-ci.config.js` - Used when running in CI or to debug CI issues locally
- `./playwright-local.config.js` - Used when running locally
- `./playwright-performance.config.js` - Used when running performance tests in CI or locally
- `./playwright-visual.config.js` - Used to run the visual tests in CI or locally
|Config File|Description|
|:-:|-|
|`./playwright-ci.config.js` | Used when running in CI or to debug CI issues locally|
|`./playwright-local.config.js` | Used when running locally|
|`./playwright-performance.config.js` | Used when running performance tests in CI or locally|
|`./playwright-visual.config.js` | Used to run the visual tests in CI or locally|
#### Test Tags
@@ -169,13 +173,15 @@ Test tags are a great way of organizing tests outside of a file structure. To le
Current list of test tags:
- `@ipad` - Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no Create button).
- `@gds` - Denotes a GDS Test Case used in the VIPER Mission.
- `@addInit` - Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.
- `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).
- `@snapshot` - Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.
- `@unstable` - A new test or test which is known to be flaky.
- `@2p` - Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.
|Test Tag|Description|
|:-:|-|
|`@ipad` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|`@gds` | Denotes a GDS Test Case used in the VIPER Mission.|
|`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.|
|`@localStorage` | Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).|
|`@snapshot` | Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.|
|`@unstable` | A new test or test which is known to be flaky.|
|`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.|
### Continuous Integration
@@ -232,7 +238,8 @@ At the same time, we don't want to waste CI resources on parallel runs, so we've
In order to maintain fast and reliable feedback, tests go through a promotion process. All new test cases or test suites must be labeled with the `@unstable` annotation. The Open MCT dev team runs these unstable tests in our private repos to ensure they work downstream and are reliable.
To run the stable tests, use the ```npm run test:e2e:stable``` command. To run the new and flaky tests, use the ```npm run test:e2e:unstable``` command.
- To run the stable tests, use the `npm run test:e2e:stable` command.
- To run the new and flaky tests, use the `npm run test:e2e:unstable` command.
A testcase and testsuite are to be unmarked as @unstable when:
@@ -293,13 +300,24 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
- How to make tests robust to function in other contexts (VISTA, VIPER, etc.)
- Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`
- How to make tests faster and more resilient
- When possible, navigate directly by URL
- Leverage `await page.goto('./', { waitUntil: 'networkidle' });`
- When possible, navigate directly by URL:
```javascript
// You can capture the CreatedObjectInfo returned from this appAction:
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
// ...and use its `url` property to navigate directly to it later in the test:
await page.goto(clock.url);
```
- Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
- Initial navigation should _almost_ always use the `{ waitUntil: 'domcontentloaded' }` option.
- Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions.
### How to write a great test (WIP)
- Use our [App Actions](./appActions.js) for performing common actions whenever applicable.
- Use `waitForPlotsToRender()` before asserting against anything that is dependent upon plot series data being loaded and drawn.
- If you create an object outside of using the `createDomainObjectWithDefaults` App Action, make sure to fill in the 'Notes' section of your object with `page.testNotes`:
```js
@@ -346,7 +364,7 @@ We leverage the following official Playwright reporters:
- Tracefile
- Screenshots
When running the tests locally with the `npm run test:local` command, the html report will open automatically on failure. Inside this HTML report will be a complete summary of the finished tests. If the tests failed, you'll see embedded links to screenshot failure, execution logs, and the Tracefile.
When running the tests locally with the `npm run test:e2e:local` command, the html report will open automatically on failure. Inside this HTML report will be a complete summary of the finished tests. If the tests failed, you'll see embedded links to screenshot failure, execution logs, and the Tracefile.
When looking at the reports run in CI, you'll leverage this same HTML Report which is hosted either in CircleCI or Github Actions as a build artifact.

View File

@@ -55,6 +55,7 @@
const Buffer = require('buffer').Buffer;
const genUuid = require('uuid').v4;
const { expect } = require('@playwright/test');
/**
* This common function creates a domain object with the default options. It is the preferred way of creating objects
@@ -405,19 +406,92 @@ async function selectInspectorTab(page, name) {
}
}
/**
* Waits and asserts that all plot series data on the page
* is loaded and drawn.
*
* In lieu of a better way to detect when a plot is done rendering,
* we [attach a class to the '.gl-plot' element](https://github.com/nasa/openmct/blob/5924d7ea95a0c2d4141c602a3c7d0665cb91095f/src/plugins/plot/MctPlot.vue#L27)
* once all pending series data has been loaded. The following appAction retrieves
* all plots on the page and waits up to the default timeout for the class to be
* attached to each plot.
* @param {import('@playwright/test').Page} page
*/
async function waitForPlotsToRender(page) {
const plotLocator = page.locator('.gl-plot');
for (const plot of await plotLocator.all()) {
await expect(plot).toHaveClass(/js-series-data-loaded/);
}
}
/**
* @typedef {Object} PlotPixel
* @property {number} r The value of the red channel (0-255)
* @property {number} g The value of the green channel (0-255)
* @property {number} b The value of the blue channel (0-255)
* @property {number} a The value of the alpha channel (0-255)
* @property {string} strValue The rgba string value of the pixel
*/
/**
* Wait for all plots to render and then retrieve and return an array
* of canvas plot pixel data (RGBA values).
* @param {import('@playwright/test').Page} page
* @param {string} canvasSelector The selector for the canvas element
* @return {Promise<PlotPixel[]>}
*/
async function getCanvasPixels(page, canvasSelector) {
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve));
const canvasHandle = await page.evaluateHandle((canvas) => document.querySelector(canvas), canvasSelector);
const canvasContextHandle = await page.evaluateHandle(canvas => canvas.getContext('2d'), canvasHandle);
await waitForPlotsToRender(page);
await page.evaluate(([canvas, ctx]) => {
// The document canvas is where the plot points and lines are drawn.
// The only way to access the canvas is using document (using page.evaluate)
/** @type {ImageData} */
const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
/** @type {number[]} */
const imageDataValues = Object.values(data);
/** @type {PlotPixel[]} */
const plotPixels = [];
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
for (let i = 0; i < imageDataValues.length;) {
if (imageDataValues[i] > 0) {
plotPixels.push({
r: imageDataValues[i],
g: imageDataValues[i + 1],
b: imageDataValues[i + 2],
a: imageDataValues[i + 3],
strValue: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})`
});
}
i = i + 4;
}
window.getCanvasValue(plotPixels);
}, [canvasHandle, canvasContextHandle]);
return getTelemValuePromise;
}
// eslint-disable-next-line no-undef
module.exports = {
createDomainObjectWithDefaults,
createNotification,
expandTreePaneItemByName,
expandEntireTree,
createPlanFromJSON,
openObjectTreeContextMenu,
expandEntireTree,
expandTreePaneItemByName,
getCanvasPixels,
getHashUrlToDomainObject,
getFocusedObjectUuid,
openObjectTreeContextMenu,
setFixedTimeMode,
setRealTimeMode,
setStartOffset,
setEndOffset,
selectInspectorTab
selectInspectorTab,
waitForPlotsToRender
};

View File

@@ -26,7 +26,7 @@ necessarily be used for reference when writing new tests in this area.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
const { createDomainObjectWithDefaults, getCanvasPixels, selectInspectorTab, waitForPlotsToRender } = require('../../../../appActions');
test.describe('Overlay Plot', () => {
test.beforeEach(async ({ page }) => {
@@ -52,14 +52,9 @@ test.describe('Overlay Plot', () => {
await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click();
await page.locator('.c-click-swatch--menu').click();
await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click();
// gets color for swatch located in legend
const element = await page.waitForSelector('.plot-series-color-swatch');
const color = await element.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-color');
});
expect(color).toBe('rgb(255, 166, 61)');
const seriesColorSwatch = page.locator('.gl-plot-label > .plot-series-color-swatch');
await expect(seriesColorSwatch).toHaveCSS('background-color', 'rgb(255, 166, 61)');
});
test('Limit lines persist when series is moved to another Y Axis and on refresh', async ({ page }) => {
@@ -215,66 +210,26 @@ test.describe('Overlay Plot', () => {
await page.goto(overlayPlot.url);
// Wait for plot series data to load and be drawn
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
await waitForPlotsToRender(page);
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Elements');
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
// Wait for "View Large" plot series data to load and be drawn
await expect(page.locator('.c-overlay .js-series-data-loaded')).toBeVisible();
const plotPixelSize = await getCanvasPixelsWithData(page);
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');
const plotPixelSize = plotPixels.length;
expect(plotPixelSize).toBeGreaterThan(0);
});
});
/**
* @param {import('@playwright/test').Page} page
*/
async function getCanvasPixelsWithData(page) {
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve));
await page.evaluate(() => {
// The document canvas is where the plot points and lines are drawn.
// The only way to access the canvas is using document (using page.evaluate)
let data;
let canvas;
let ctx;
canvas = document.querySelector('.js-overlay canvas');
ctx = canvas.getContext('2d');
data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
const imageDataValues = Object.values(data);
let plotPixels = [];
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
for (let i = 0; i < imageDataValues.length;) {
if (imageDataValues[i] > 0) {
plotPixels.push({
startIndex: i,
endIndex: i + 3,
value: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})`
});
}
i = i + 4;
}
window.getCanvasValue(plotPixels.length);
});
return getTelemValuePromise;
}
/**
*
* Asserts that limit lines exist and are visible
* @param {import('@playwright/test').Page} page
*/
async function assertLimitLinesExistAndAreVisible(page) {
// Wait for plot series data to load
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
await waitForPlotsToRender(page);
// Wait for limit lines to be created
await page.waitForSelector('.js-limit-area', { state: 'attached' });
const limitLineCount = await page.locator('.c-plot-limit-line').count();

View File

@@ -1,113 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
Tests to verify log plot functionality. Note this test suite if very much under active development and should not
necessarily be used for reference when writing new tests in this area.
*/
const { selectInspectorTab } = require('../../../../appActions');
const { test, expect } = require('../../../../pluginFixtures');
test.describe('Legend color in sync with plot color', () => {
test('Testing', async ({ page }) => {
await makeOverlayPlot(page);
// navigate to plot series color palette
await page.click('.l-browse-bar__actions__edit');
await selectInspectorTab(page, 'Config');
await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click();
await page.locator('.c-click-swatch--menu').click();
await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click();
// gets color for swatch located in legend
const element = await page.waitForSelector('.plot-series-color-swatch');
const color = await element.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-color');
});
expect(color).toBe('rgb(255, 166, 61)');
});
});
async function saveOverlayPlot(page) {
// save overlay plot
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
await Promise.all([
page.locator('text=Save and Finish Editing').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
}
async function makeOverlayPlot(page) {
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
await page.goto('/', { waitUntil: 'networkidle' });
// create overlay plot
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// save the overlay plot
await saveOverlayPlot(page);
// create a sinewave generator
await page.locator('button.c-create-button').click();
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
// Click OK to make generator
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('button:has-text("OK")').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// click on overlay plot
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Overlay Plot').first().click()
]);
}

View File

@@ -26,26 +26,25 @@
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults} = require('../../../../appActions');
const { createDomainObjectWithDefaults, getCanvasPixels } = require('../../../../appActions');
test.describe('Plot Integrity Testing @unstable', () => {
test.describe('Plot Rendering', () => {
let sineWaveGeneratorObject;
test.beforeEach(async ({ page }) => {
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'domcontentloaded' });
sineWaveGeneratorObject = await createDomainObjectWithDefaults(page, { type: 'Sine Wave Generator' });
});
test('Plots do not re-request data when a plot is clicked', async ({ page }) => {
//Navigate to Sine Wave Generator
// Navigate to Sine Wave Generator
await page.goto(sineWaveGeneratorObject.url);
//Click on the plot canvas
// Click on the plot canvas
await page.locator('canvas').nth(1).click();
//No request was made to get historical data
// No request was made to get historical data
const createMineFolderRequests = [];
page.on('request', req => {
// eslint-disable-next-line playwright/no-conditional-in-test
createMineFolderRequests.push(req);
});
expect(createMineFolderRequests.length).toEqual(0);
@@ -56,7 +55,8 @@ test.describe('Plot Integrity Testing @unstable', () => {
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);
//Get pixel data from Canvas
const plotPixelSize = await getCanvasPixelsWithData(page);
const plotPixels = await getCanvasPixels(page, 'canvas');
const plotPixelSize = plotPixels.length;
expect(plotPixelSize).toBeGreaterThan(0);
});
});
@@ -70,70 +70,19 @@ test.describe('Plot Integrity Testing @unstable', () => {
*/
async function editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject) {
await page.goto(sineWaveGeneratorObject.url);
// Edit LAD table
// Edit SWG properties to include infinity values
await page.locator('[title="More options"]').click();
await page.locator('[title="Edit properties of this object."]').click();
// Modify the infinity option to true
const infinityInput = page.locator('[aria-label="Include Infinity Values"]');
await infinityInput.click();
await page.getByRole('switch', {
name: "Include Infinity Values"
}).check();
// Click OK button and wait for Navigate event
await Promise.all([
page.waitForLoadState(),
page.click('[aria-label="Save"]'),
// Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
await page.getByRole('button', {
name: 'Save'
}).click();
// FIXME: Changes to SWG properties should be reflected on save, but they're not?
// Thus, navigate away and back to the object.
await page.goto('./#/browse/mine');
await page.goto(sineWaveGeneratorObject.url);
await page.locator('c-progress-bar c-telemetry-table__progress-bar').waitFor({
state: 'hidden'
});
// FIXME: The progress bar disappears on series data load, not on plot render,
// so wait for a half a second before evaluating the canvas.
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(500);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function getCanvasPixelsWithData(page) {
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve));
await page.evaluate(() => {
// The document canvas is where the plot points and lines are drawn.
// The only way to access the canvas is using document (using page.evaluate)
let data;
let canvas;
let ctx;
canvas = document.querySelector('canvas');
ctx = canvas.getContext('2d');
data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
const imageDataValues = Object.values(data);
let plotPixels = [];
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
for (let i = 0; i < imageDataValues.length;) {
if (imageDataValues[i] > 0) {
plotPixels.push({
startIndex: i,
endIndex: i + 3,
value: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})`
});
}
i = i + 4;
}
window.getCanvasValue(plotPixels.length);
});
return getTelemValuePromise;
}

View File

@@ -25,7 +25,7 @@ Tests to verify plot tagging functionality.
*/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, setRealTimeMode, setFixedTimeMode } = require('../../../../appActions');
const { createDomainObjectWithDefaults, setRealTimeMode, setFixedTimeMode, waitForPlotsToRender } = require('../../../../appActions');
test.describe('Plot Tagging', () => {
/**
@@ -133,12 +133,9 @@ test.describe('Plot Tagging', () => {
await expect(page.getByText('No results found')).toBeVisible();
//Reload Page
await Promise.all([
page.reload(),
page.waitForLoadState('networkidle')
]);
await page.reload({ waitUntil: 'domcontentloaded' });
// wait for plots to load
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
await waitForPlotsToRender(page);
await page.getByText('Annotations').click();
await expect(page.getByText('No tags to display for this item')).toBeVisible();

View File

@@ -0,0 +1,74 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { expect, test } = require('../../pluginFixtures');
const percySnapshot = require('@percy/playwright');
const { createDomainObjectWithDefaults } = require('../../appActions');
test.describe('Visual - LAD Table', () => {
/** @type {import('@playwright/test').Locator} */
let ladTable;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create LAD Table
ladTable = await createDomainObjectWithDefaults(page, {
type: 'LAD Table',
name: 'LAD Table Test'
});
// Create SWG inside of LAD Table
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: 'SWG4LAD Table Test',
parent: ladTable.uuid
});
//Modify SWG to create a really stable SWG
await page.locator('button[title="More options"]').click();
await page.getByRole('menuitem', { name: ' Edit Properties...' }).click();
//Forgive me, padre
await page.getByRole('spinbutton', { name: 'Data Rate (hz)' }).fill('0');
await page.getByRole('spinbutton', { name: 'Period' }).fill('0');
await page.getByRole('button', { name: 'Save' }).click();
});
test('Toggled column widths behave accordingly', async ({ page, theme }) => {
await page.goto(ladTable.url);
//Close panes for visual consistency
await page.getByTitle('Collapse Inspect Pane').click();
await page.getByTitle('Collapse Browse Pane').click();
await expect(page.locator('button[title="Expand Columns"]')).toBeVisible();
await percySnapshot(page, `LAD Table w/ Sine Wave Generator columns autosized (theme: ${theme})`);
await page.locator('button[title="Expand Columns"]').click();
await expect(page.locator('button[title="Autosize Columns"]')).toBeVisible();
await percySnapshot(page, `LAD Table w/ Sine Wave Generator columns expanded (theme: ${theme})`);
});
});

View File

@@ -3,10 +3,10 @@
"version": "2.2.3-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
"@babel/eslint-parser": "7.19.1",
"@braintree/sanitize-url": "6.0.2",
"@deploysentinel/playwright": "0.3.4",
"@percy/cli": "1.23.0",
"@percy/cli": "1.24.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.32.3",
"@types/eventemitter3": "1.2.0",
@@ -21,7 +21,7 @@
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.37.0",
"eslint": "8.39.0",
"eslint-plugin-compat": "4.1.4",
"eslint-plugin-playwright": "0.12.0",
"eslint-plugin-vue": "9.11.0",
@@ -32,8 +32,8 @@
"html2canvas": "1.4.1",
"imports-loader": "4.0.1",
"jasmine-core": "4.5.0",
"karma": "6.3.20",
"karma-chrome-launcher": "3.1.1",
"karma": "6.4.2",
"karma-chrome-launcher": "3.2.0",
"karma-cli": "2.0.0",
"karma-coverage": "2.2.0",
"karma-coverage-istanbul-reporter": "3.0.3",
@@ -57,7 +57,7 @@
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sanitize-html": "2.10.0",
"sass": "1.62.0",
"sass": "1.62.1",
"sass-loader": "13.2.2",
"sinon": "15.0.1",
"style-loader": "3.3.2",
@@ -67,8 +67,8 @@
"vue-eslint-parser": "9.1.0",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.6.14",
"webpack": "5.79.0",
"webpack-cli": "5.0.0",
"webpack": "5.81.0",
"webpack-cli": "5.0.2",
"webpack-dev-server": "4.13.3",
"webpack-merge": "5.8.0"
},

View File

@@ -134,11 +134,6 @@ class TimeAPI extends GlobalTimeContext {
*/
addIndependentContext(key, value, clockKey) {
let timeContext = this.getIndependentContext(key);
let upstreamClock;
if (timeContext.upstreamTimeContext) {
upstreamClock = timeContext.upstreamTimeContext.clock();
}
//stop following upstream time context since the view has it's own
timeContext.resetContext();
@@ -146,11 +141,6 @@ class TimeAPI extends GlobalTimeContext {
timeContext.clock(clockKey, value);
} else {
timeContext.stopClock();
//upstream clock was active, but now we don't have one
if (upstreamClock) {
timeContext.emit('clock', timeContext.activeClock);
}
timeContext.bounds(value);
}

View File

@@ -22,25 +22,12 @@
import EventEmitter from 'EventEmitter';
export const TIME_CONTEXT_EVENTS = {
//old API events - to be deprecated
bounds: 'bounds',
clock: 'clock',
timeSystem: 'timeSystem',
clockOffsets: 'clockOffsets',
//new API events
tick: 'tick',
modeChanged: 'modeChanged',
boundsChanged: 'boundsChanged',
clockChanged: 'clockChanged',
timeSystemChanged: 'timeSystemChanged',
clockOffsetsChanged: 'clockOffsetsChanged'
};
export const MODES = {
fixed: 'fixed',
realtime: 'realtime'
};
export const TIME_CONTEXT_EVENTS = [
'bounds',
'clock',
'timeSystem',
'clockOffsets'
];
class TimeContext extends EventEmitter {
constructor() {
@@ -60,7 +47,6 @@ class TimeContext extends EventEmitter {
this.activeClock = undefined;
this.offsets = undefined;
this.mode = undefined;
this.tick = this.tick.bind(this);
}
@@ -292,7 +278,7 @@ class TimeContext extends EventEmitter {
}
/**
* Stop following the currently active clock. This will
* Stop the currently active clock from ticking, and unset it. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
@@ -375,7 +361,6 @@ class TimeContext extends EventEmitter {
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, true);
}
/**
@@ -389,278 +374,6 @@ class TimeContext extends EventEmitter {
return false;
}
/**
* Get the time system of the TimeAPI.
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
*/
getTimeSystem() {
return this.system;
}
/**
* Set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystemOrKey
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method setTimeSystem
*/
setTimeSystem(timeSystemOrKey, bounds) {
if (!this.isRealTime() && !bounds) {
throw new Error(
"Must specify bounds when changing time system without "
+ "an active clock."
);
}
if (timeSystemOrKey === undefined) {
throw "Please provide a time system";
}
let timeSystem;
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
if (!this.timeSystems.has(timeSystem.key)) {
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
}
} else {
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
}
this.system = timeSystem;
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, this.system);
if (bounds) {
this.setBounds(bounds);
}
return this.system;
}
/**
* Get the start and end time of the time conductor. Basic validation
* of bounds is performed.
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
getBounds() {
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
* Set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
setBounds(newBounds) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (i.e. was an automatic update), false otherwise.
*/
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, false);
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
* Get the active clock.
* @return {Clock} the currently active clock;
*/
getClock() {
return this.activeClock;
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and the currently ticking will begin.
* Offsets from 'now', if provided, will be used to set realtime mode offsets
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds when in realtime mode.
* This maintains a sliding time window of a fixed width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
setClock(keyOrClock, offsets) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
const isRealtimeMode = this.getMode() === MODES.realtime;
const previousClock = this.activeClock;
if (previousClock !== undefined && isRealtimeMode) {
previousClock.off("tick", this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
if (isRealtimeMode) {
if (this.activeClock !== undefined) {
this.activeClock.on("tick", this.tick);
}
if (offsets !== undefined) {
this.clockOffsets(offsets);
}
}
return this.activeClock;
}
/**
* Get the current mode.
* @return {Mode} the current mode;
*/
getMode() {
return this.mode;
}
/**
* Set the mode to either fixed or realtime.
*
* @param {Mode} mode The mode to activate
* @param {ClockOffsets | Bounds} offsets on each tick these will be used to calculate
* the start and end bounds. In realtime mode, this maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
*/
setMode(mode, offsets) {
if (offsets === undefined) {
throw "When setting the mode, offsets must also be provided";
}
const previousMode = this.mode;
if (previousMode === MODES.realtime) {
this.activeClock.off('tick', this.tick);
}
this.mode = mode;
if (mode === MODES.realtime) {
this.activeClock.on("tick", this.tick);
this.setClockOffsets(offsets);
} else if (mode === MODES.fixed) {
this.activeClock.off("tick", this.tick);
this.setBounds(offsets);
}
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.mode);
return this.mode;
}
/**
* Get the currently applied clock offsets.
* @returns {ClockOffsets}
*/
getClockOffsets() {
return this.offsets;
}
/**
* Set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
setClockOffsets(offsets) {
const validationResult = this.validateOffsets(offsets);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
this.offsets = offsets;
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
this.setBounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit(TIME_CONTEXT_EVENTS.clockOffsetsChanged, offsets);
return this.offsets;
}
}
export default TimeContext;

View File

@@ -226,7 +226,10 @@ export default {
};
},
toggleFixedLayout() {
this.configuration.isFixedLayout = !this.configuration.isFixedLayout;
const config = structuredClone(this.configuration);
config.isFixedLayout = !this.configuration.isFixedLayout;
this.ladTableConfiguration.updateConfiguration(config);
},
initializeViewActions() {
if (this.configuration.isFixedLayout) {

View File

@@ -21,16 +21,18 @@
*****************************************************************************/
<template>
<component
:is="urlDefined ? 'a' : 'span'"
<div
ref="conditionWidgetElement"
class="c-condition-widget u-style-receiver js-style-receiver"
:href="url"
:target="url ? '_BLANK' : ''"
>
<div class="c-condition-widget__label">
{{ label }}
</div>
</component>
<component
:is="urlDefined ? 'a' : 'div'"
class="c-condition-widget__label-wrapper"
:href="url"
>
<div class="c-condition-widget__label">{{ label }}</div>
</component>
</div>
</template>
<script>
@@ -40,19 +42,26 @@ export default {
inject: ['openmct', 'domainObject'],
data: function () {
return {
conditionalLabel: '',
conditionSetIdentifier: null,
domainObjectLabel: '',
url: null,
urlDefined: false,
useConditionSetOutputAsLabel: false
conditionalLabel: ''
};
},
computed: {
urlDefined() {
return this.domainObject.url?.length > 0;
},
url() {
return this.urlDefined ? sanitizeUrl(this.domainObject.url) : null;
},
useConditionSetOutputAsLabel() {
return this.conditionSetIdentifier && this.domainObject.configuration.useConditionSetOutputAsLabel;
},
conditionSetIdentifier() {
return this.domainObject.configuration?.objectStyles?.conditionSetIdentifier;
},
label() {
return this.useConditionSetOutputAsLabel
? this.conditionalLabel
: this.domainObjectLabel
: this.domainObject.label
;
}
},
@@ -69,20 +78,11 @@ export default {
}
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.updateDomainObject);
if (this.domainObject) {
this.updateDomainObject(this.domainObject);
this.listenToConditionSetChanges();
}
},
beforeDestroy() {
this.conditionSetIdentifier = null;
if (this.unlisten) {
this.unlisten();
}
this.stopListeningToConditionSetChanges();
},
methods: {
@@ -121,31 +121,6 @@ export default {
}
this.conditionalLabel = latestDatum.output || '';
},
updateDomainObject(domainObject) {
if (this.domainObjectLabel !== domainObject.label) {
this.domainObjectLabel = domainObject.label;
}
const urlDefined = domainObject.url && domainObject.url.length > 0;
if (this.urlDefined !== urlDefined) {
this.urlDefined = urlDefined;
}
const url = this.urlDefined ? sanitizeUrl(domainObject.url) : null;
if (this.url !== url) {
this.url = url;
}
const conditionSetIdentifier = domainObject.configuration?.objectStyles?.conditionSetIdentifier;
if (conditionSetIdentifier && this.conditionSetIdentifier !== conditionSetIdentifier) {
this.conditionSetIdentifier = conditionSetIdentifier;
}
const useConditionSetOutputAsLabel = this.conditionSetIdentifier && domainObject.configuration.useConditionSetOutputAsLabel;
if (this.useConditionSetOutputAsLabel !== useConditionSetOutputAsLabel) {
this.useConditionSetOutputAsLabel = useConditionSetOutputAsLabel;
}
}
}
};

View File

@@ -26,31 +26,35 @@
background-color: rgba($colorBodyFg, 0.1); // Give a little presence if the user hasn't defined a fill color
border-radius: $basicCr;
border: 1px solid transparent;
display: inline-block;
padding: $interiorMarginLg $interiorMarginLg * 2;
display: block;
max-width: max-content;
a {
display: block;
color: inherit;
}
}
.c-condition-widget__label {
padding: $interiorMargin;
// Either a <div> or an <a> tag
padding: $interiorMargin $interiorMargin * 1.5;
text-align: center;
white-space: normal;
}
a.c-condition-widget {
// Widget is conditionally made into a <a> when URL property has been defined
cursor: pointer !important;
pointer-events: inherit;
}
// Make Condition Widget expand when in a hidden frame Layout context
// For both static and Flexible Layouts
.c-so-view--conditionWidget.c-so-view--no-frame {
.c-condition-widget {
@include abs();
display: flex;
align-items: center;
justify-content: center;
padding: 0;
max-width: unset;
&__label-wrapper {
@include abs();
display: flex;
align-items: center;
justify-content: center;
}
}
.c-so-view__frame-controls { display: none; }

View File

@@ -36,6 +36,7 @@ export default function plugin() {
domainObject.configuration = {};
domainObject.label = 'Condition Widget';
domainObject.conditionalLabel = '';
domainObject.url = '';
},
form: [
{

View File

@@ -19,7 +19,7 @@
class="c-icon-button c-button--menu icon-font"
@click.prevent.stop="showFontMenu"
>
<span class="c-button__label">{{ fontTypeLable }}</span>
<span class="c-button__label">{{ fontTypeLabel }}</span>
</button>
</div>
</div>
@@ -43,7 +43,7 @@ export default {
}
},
computed: {
fontTypeLable() {
fontTypeLabel() {
const fontType = FONTS.find(f => f.value === this.fontStyle.font);
if (!fontType) {
return '??';

View File

@@ -24,6 +24,7 @@ import CouchDocument from "./CouchDocument";
import CouchObjectQueue from "./CouchObjectQueue";
import { PENDING, CONNECTED, DISCONNECTED, UNKNOWN } from "./CouchStatusIndicator";
import { isNotebookOrAnnotationType } from '../../notebook/notebook-constants.js';
import _ from 'lodash';
const REV = "_rev";
const ID = "_id";
@@ -42,6 +43,8 @@ class CouchObjectProvider {
this.batchIds = [];
this.onEventMessage = this.onEventMessage.bind(this);
this.onEventError = this.onEventError.bind(this);
this.flushPersistenceQueue = _.debounce(this.flushPersistenceQueue.bind(this));
this.persistenceQueue = [];
}
/**
@@ -666,7 +669,11 @@ class CouchObjectProvider {
const queued = this.objectQueue[key].dequeue();
let document = new CouchDocument(key, queued.model);
document.metadata.created = Date.now();
this.request(key, "PUT", document).then((response) => {
this.put({
key,
document
}).then((response) => {
this.#checkResponse(response, queued.intermediateResponse, key);
}).catch(error => {
queued.intermediateResponse.reject(error);
@@ -677,6 +684,50 @@ class CouchObjectProvider {
return intermediateResponse.promise;
}
put({key, document}) {
return new Promise((resolve, reject) => {
this.persistenceQueue.push({
key,
document,
resolve,
reject
});
this.flushPersistenceQueue();
});
}
async flushPersistenceQueue() {
if (this.persistenceQueue.length > 1) {
const batch = {
docs: this.persistenceQueue.map((queued) => queued.document)
};
const response = await this.request("_bulk_docs", "POST", batch);
response.forEach((responseMetadatum) => {
const queued = this.persistenceQueue.find((queuedMetadatum) => queuedMetadatum.key === responseMetadatum.id);
if (responseMetadatum.ok) {
queued.resolve(responseMetadatum);
} else {
queued.reject(responseMetadatum);
}
});
} else if (this.persistenceQueue.length === 1) {
const {
key,
document,
resolve,
reject
} = this.persistenceQueue[0];
this.request(key, "PUT", document)
.then(resolve)
.catch(reject);
}
this.persistenceQueue = [];
}
/**
* @private
*/

View File

@@ -1,10 +1,9 @@
version: "3"
services:
couchdb:
image: couchdb:${COUCHDB_IMAGE_TAG:-3.2.1}
image: couchdb:${COUCHDB_IMAGE_TAG:-3.3.2}
ports:
- "5984:5984"
- "5986:5986"
volumes:
- couchdb:/opt/couchdb/data
environment:

View File

@@ -1,57 +1,25 @@
#!/bin/bash -e
# Do a couple checks for environment variables we expect to have a value.
if [ -z "${OPENMCT_DATABASE_NAME}" ] ; then
echo "OPENMCT_DATABASE_NAME has no value" 1>&2
exit 1
fi
if [ -z "${COUCH_ADMIN_USER}" ] ; then
echo "COUCH_ADMIN_USER has no value" 1>&2
exit 1
fi
if [ -z "${COUCH_BASE_LOCAL}" ] ; then
echo "COUCH_BASE_LOCAL has no value" 1>&2
exit 1
fi
# Come up with what we'll be providing to curl's -u option. Always supply the username from the environment,
# and optionally supply the password from the environment, if it has a value.
CURL_USERPASS_ARG="${COUCH_ADMIN_USER}"
if [ "${COUCH_ADMIN_PASSWORD}" ] ; then
CURL_USERPASS_ARG+=":${COUCH_ADMIN_PASSWORD}"
fi
system_tables_exist () {
resource_exists $COUCH_BASE_LOCAL/_users
}
create_users_db () {
curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/_users
}
create_replicator_db () {
curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/_replicator
}
setup_system_tables () {
users_db_response=$(create_users_db)
if [ "{\"ok\":true}" == "${users_db_response}" ]; then
echo Successfully created users db
replicator_db_response=$(create_replicator_db)
if [ "{\"ok\":true}" == "${replicator_db_response}" ]; then
echo Successfully created replicator DB
else
echo Unable to create replicator DB
fi
else
echo Unable to create users db
# Check if required environment variables have values, exit if not.
check_env_var() {
if [ -z "$1" ]; then
echo "$2 has no value" 1>&2
exit 1
fi
}
resource_exists () {
check_env_var "${OPENMCT_DATABASE_NAME}" "OPENMCT_DATABASE_NAME"
check_env_var "${COUCH_ADMIN_USER}" "COUCH_ADMIN_USER"
check_env_var "${COUCH_BASE_LOCAL}" "COUCH_BASE_LOCAL"
# Construct curl's -u option value based on COUCH_ADMIN_USER and COUCH_ADMIN_PASSWORD environment variables.
CURL_USERPASS_ARG="${COUCH_ADMIN_USER}"
if [ "${COUCH_ADMIN_PASSWORD}" ]; then
CURL_USERPASS_ARG+=":${COUCH_ADMIN_PASSWORD}"
fi
# Functions
resource_exists() {
response=$(curl -u "${CURL_USERPASS_ARG}" -s -o /dev/null -I -w "%{http_code}" $1);
if [ "200" == "${response}" ]; then
echo "TRUE"
@@ -60,16 +28,16 @@ resource_exists () {
fi
}
db_exists () {
db_exists() {
resource_exists $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME
}
create_db () {
create_db() {
response=$(curl -su "${CURL_USERPASS_ARG}" -XPUT $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME);
echo $response
}
admin_user_exists () {
admin_user_exists() {
response=$(curl -su "${CURL_USERPASS_ARG}" -o /dev/null -I -w "%{http_code}" $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/admins/$COUCH_ADMIN_USER);
if [ "200" == "${response}" ]; then
echo "TRUE"
@@ -78,7 +46,7 @@ admin_user_exists () {
fi
}
create_admin_user () {
create_admin_user() {
echo Creating admin user
curl -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/admins/$COUCH_ADMIN_USER -d \'"$COUCH_ADMIN_PASSWORD"\'
}
@@ -87,7 +55,7 @@ is_cors_enabled() {
resource_exists $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/httpd/enable_cors
}
enable_cors () {
enable_cors() {
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/httpd/enable_cors -d '"true"'
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/origins -d '"*"'
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/credentials -d '"true"'
@@ -95,6 +63,36 @@ enable_cors () {
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/headers -d '"accept, authorization, content-type, origin, referer, x-csrf-token"'
}
update_db_permissions() {
local db_name=$1
echo "Updating ${db_name} database permissions"
response=$(curl -su "${CURL_USERPASS_ARG}" --location \
--request PUT $COUCH_BASE_LOCAL/$db_name/_security \
--header 'Content-Type: application/json' \
--data-raw '{ "admins": {"roles": []},"members": {"roles": []}}')
if [ "{\"ok\":true}" == "${response}" ]; then
echo "Database permissions successfully updated"
else
echo "Database permissions not updated"
fi
}
create_system_tables() {
local system_tables=("_users" "_replicator")
for table in "${system_tables[@]}"; do
echo "Creating $table database"
response=$(curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/$table)
if [ "{\"ok\":true}" == "${response}" ]; then
echo "Successfully created $table database"
else
echo "Unable to create $table database"
fi
done
}
# Main script execution
# Check if the admin user exists; if not, create it.
if [ "$(admin_user_exists)" == "FALSE" ]; then
echo "Admin user does not exist, creating..."
create_admin_user
@@ -102,40 +100,32 @@ else
echo "Admin user exists"
fi
if [ "TRUE" == $(system_tables_exist) ]; then
echo System tables exist, skipping creation
# Check if system tables exist; if not, create them.
system_tables_exist=$(resource_exists $COUCH_BASE_LOCAL/_users)
if [ "TRUE" == "${system_tables_exist}" ]; then
echo "System tables exist, skipping creation"
else
echo Is fresh install, creating system tables
setup_system_tables
echo "Fresh install, creating system tables"
create_system_tables
fi
# Check if the database exists; if not, create it.
if [ "FALSE" == $(db_exists) ]; then
response=$(create_db)
if [ "{\"ok\":true}" == "${response}" ]; then
echo Database successfully created
echo "Database successfully created"
else
echo Database creation failed
echo "Database creation failed"
fi
else
echo Database already exists, nothing to do
echo "Database already exists, nothing to do"
fi
echo "Updating _replicator database permissions"
response=$(curl -su "${CURL_USERPASS_ARG}" --location --request PUT $COUCH_BASE_LOCAL/_replicator/_security --header 'Content-Type: application/json' --data-raw '{ "admins": {"roles": []},"members": {"roles": []}}');
if [ "{\"ok\":true}" == "${response}" ]; then
echo "Database permissions successfully updated"
else
echo "Database permissions not updated"
fi
echo "Updating ${OPENMCT_DATABASE_NAME} database permissions"
response=$(curl -su "${CURL_USERPASS_ARG}" --location --request PUT $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME/_security --header 'Content-Type: application/json' --data-raw '{ "admins": {"roles": []},"members": {"roles": []}}');
if [ "{\"ok\":true}" == "${response}" ]; then
echo "Database permissions successfully updated"
else
echo "Database permissions not updated"
fi
# Update _replicator and OPENMCT_DATABASE_NAME database permissions
update_db_permissions "_replicator"
update_db_permissions "${OPENMCT_DATABASE_NAME}"
# Check if CORS is enabled; if not, enable it.
if [ "FALSE" == $(is_cors_enabled) ]; then
echo "Enabling CORS"
enable_cors

View File

@@ -21,8 +21,7 @@
*****************************************************************************/
<template>
<div
ref="timeConductorOptionsHolder"
class="c-compact-tc is-expanded"
class="c-conductor"
:class="[
{ 'is-zooming': isZooming },
{ 'is-panning': isPanning },
@@ -30,44 +29,52 @@
isFixed ? 'is-fixed-mode' : 'is-realtime-mode'
]"
>
<ConductorModeIcon class="c-conductor__mode-icon" />
<!-- TODO - NEED TO ADD MODE, CLOCK AND TIMESYSTEM VIEW ONLY INFORMATION HERE -->
<conductor-inputs-fixed
v-if="isFixed"
:input-bounds="viewBounds"
:read-only="true"
/>
<conductor-inputs-realtime
v-else
:input-bounds="viewBounds"
:read-only="true"
/>
<conductor-axis
v-if="isFixed"
class="c-conductor__ticks"
:view-bounds="viewBounds"
:is-fixed="isFixed"
:alt-pressed="altPressed"
@endPan="endPan"
@endZoom="endZoom"
@panAxis="pan"
@zoomAxis="zoom"
/>
<div
v-else
class="u-flex-spreader"
></div>
<div class="c-not-button c-not-button--compact c-compact-tc__gear icon-gear"></div>
<div class="c-conductor__time-bounds">
<conductor-inputs-fixed
v-if="isFixed"
:input-bounds="viewBounds"
@updated="saveFixedOffsets"
/>
<conductor-inputs-realtime
v-else
:input-bounds="viewBounds"
@updated="saveClockOffsets"
/>
<ConductorModeIcon class="c-conductor__mode-icon" />
<conductor-axis
class="c-conductor__ticks"
:view-bounds="viewBounds"
:is-fixed="isFixed"
:alt-pressed="altPressed"
@endPan="endPan"
@endZoom="endZoom"
@panAxis="pan"
@zoomAxis="zoom"
/>
</div>
<div class="c-conductor__controls">
<ConductorMode class="c-conductor__mode-select" />
<ConductorTimeSystem class="c-conductor__time-system-select" />
<ConductorHistory
class="c-conductor__history-select"
:offsets="openmct.time.clockOffsets()"
:bounds="bounds"
:time-system="timeSystem"
:mode="timeMode"
/>
</div>
</div>
</template>
<script>
import _ from 'lodash';
import ConductorMode from './ConductorMode.vue';
import ConductorTimeSystem from './ConductorTimeSystem.vue';
import ConductorAxis from './ConductorAxis.vue';
import ConductorModeIcon from './ConductorModeIcon.vue';
import ConductorHistory from './ConductorHistory.vue';
import ConductorInputsFixed from "./ConductorInputsFixed.vue";
import ConductorInputsRealtime from "./ConductorInputsRealtime.vue";
import conductorPopUpManager from "./conductorPopUpManager";
const DEFAULT_DURATION_FORMATTER = 'duration';
@@ -75,10 +82,12 @@ export default {
components: {
ConductorInputsRealtime,
ConductorInputsFixed,
ConductorMode,
ConductorTimeSystem,
ConductorAxis,
ConductorModeIcon
ConductorModeIcon,
ConductorHistory
},
mixins: [conductorPopUpManager],
inject: ['openmct', 'configuration'],
data() {
let bounds = this.openmct.time.bounds();
@@ -112,9 +121,16 @@ export default {
showDatePicker: false,
altPressed: false,
isPanning: false,
isZooming: false
isZooming: false,
showTCInputStart: false,
showTCInputEnd: false
};
},
computed: {
timeMode() {
return this.isFixed ? 'fixed' : 'realtime';
}
},
mounted() {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
@@ -180,6 +196,7 @@ export default {
this.isUTCBased = timeSystem.isUTCBased;
},
setViewFromClock(clock) {
// this.clearAllValidation();
this.isFixed = clock === undefined;
},
setViewFromBounds(bounds) {
@@ -193,22 +210,11 @@ export default {
format: key
}).formatter;
},
saveFixedBounds(bounds) {
this.openmct.time.bounds(bounds);
},
saveClockOffsets(offsets) {
this.openmct.time.clockOffsets(offsets);
},
saveMode(option) {
if (option.timeSystem) {
this.openmct.time.timeSystem(option.timeSystem, option.bounds);
}
if (option.clockKey === undefined) {
this.openmct.time.stopClock();
} else {
this.openmct.time.clock(option.clockKey, option.offsets);
}
saveFixedOffsets(bounds) {
this.openmct.time.bounds(bounds);
}
}
};

View File

@@ -98,10 +98,7 @@ export default {
//Respond to changes in conductor
this.openmct.time.on("timeSystem", this.setViewFromTimeSystem);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
beforeDestroy() {
clearInterval(this.resizeTimer);
setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
methods: {
setAxisDimensions() {

View File

@@ -26,9 +26,8 @@
>
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-button--menu c-button--compact c-history-button icon-history"
:class="buttonCssClass"
aria-label="Time Conductor History"
class="c-button--menu c-history-button icon-history"
@click.prevent.stop="showHistoryMenu"
>
<span class="c-button__label">History</span>
@@ -65,13 +64,6 @@ export default {
mode: {
type: String,
required: true
},
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
}
},
data() {
@@ -114,8 +106,8 @@ export default {
bounds: {
handler() {
// only for fixed time since we track offsets for realtime
this.updateMode();
if (this.isFixed) {
this.updateMode();
this.addTimespan();
}
},
@@ -124,9 +116,7 @@ export default {
offsets: {
handler() {
this.updateMode();
if (!this.isFixed) {
this.addTimespan();
}
this.addTimespan();
},
deep: true
},

View File

@@ -1,34 +1,78 @@
<template>
<time-popup-fixed
v-if="readOnly === false"
:input-bounds="bounds"
:input-time-system="timeSystem"
@focus.native="$event.target.select()"
@update="setBoundsFromView"
@dismiss="dismiss"
/>
<div
v-else
class="c-compact-tc__bounds"
<form
ref="fixedDeltaInput"
class="c-conductor__inputs"
>
<div class="c-compact-tc__bounds__value">{{ formattedBounds.start }}</div>
<div class="c-compact-tc__bounds__start-end-sep icon-arrows-right-left"></div>
<div class="c-compact-tc__bounds__value">{{ formattedBounds.end }}</div>
</div>
<div
class="c-ctrl-wrapper c-conductor-input c-conductor__start-fixed"
>
<!-- Fixed start -->
<div class="c-conductor__start-fixed__label">
Start
</div>
<input
ref="startDate"
v-model="formattedBounds.start"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('startDate'); submitForm()"
>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:bottom="keyString !== undefined"
:default-date-time="formattedBounds.start"
:formatter="timeFormatter"
@date-selected="startDateSelected"
/>
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed">
<!-- Fixed end and RT 'last update' display -->
<div class="c-conductor__end-fixed__label">
End
</div>
<input
ref="endDate"
v-model="formattedBounds.end"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('endDate'); submitForm()"
>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:bottom="keyString !== undefined"
:default-date-time="formattedBounds.end"
:formatter="timeFormatter"
@date-selected="endDateSelected"
/>
</div>
</form>
</template>
<script>
import TimePopupFixed from "./timePopupFixed.vue";
import DatePicker from "./DatePicker.vue";
import _ from "lodash";
// const DEFAULT_DURATION_FORMATTER = 'duration';
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
TimePopupFixed
DatePicker
},
inject: ['openmct'],
props: {
keyString: {
type: String,
default() {
return undefined;
}
},
inputBounds: {
type: Object,
default() {
@@ -40,29 +84,18 @@ export default {
default() {
return [];
}
},
readOnly: {
type: Boolean,
default() {
return false;
}
},
compact: {
type: Boolean,
default() {
return false;
}
}
},
data() {
let timeSystem = this.openmct.time.timeSystem();
// let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.bounds || this.openmct.time.bounds();
return {
timeSystem: timeSystem,
// durationFormatter,
showTCInputStart: true,
showTCInputEnd: true,
durationFormatter,
timeFormatter,
bounds: {
start: bounds.start,
@@ -76,15 +109,8 @@ export default {
};
},
watch: {
objectPath: {
handler(newPath, oldPath) {
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
keyString() {
this.setTimeContext();
},
inputBounds: {
handler(newBounds) {
@@ -100,26 +126,36 @@ export default {
this.setTimeContext();
},
beforeDestroy() {
this.clearAllValidation();
this.openmct.time.off('timeSystem', this.setTimeSystem);
this.stopFollowingTimeContext();
},
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext = this.openmct.time.getContextForView(this.keyString ? this.objectPath : []);
this.handleNewBounds(this.timeContext.bounds());
this.timeContext.on('bounds', this.handleNewBounds);
this.timeContext.on('clock', this.clearAllValidation);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
}
},
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
},
clearAllValidation() {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
},
clearValidationForInput(input) {
input.setCustomValidity('');
input.title = '';
},
setBounds(bounds) {
this.bounds = bounds;
},
@@ -130,8 +166,8 @@ export default {
setTimeSystem(timeSystem) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
// this.durationFormatter = this.getFormatter(
// timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.isUTCBased = timeSystem.isUTCBased;
},
getFormatter(key) {
@@ -139,14 +175,116 @@ export default {
format: key
}).formatter;
},
setBoundsFromView(bounds) {
this.$emit('updated', {
start: bounds.start,
end: bounds.end
setBoundsFromView($event) {
if (this.$refs.fixedDeltaInput.checkValidity()) {
let start = this.timeFormatter.parse(this.formattedBounds.start);
let end = this.timeFormatter.parse(this.formattedBounds.end);
this.$emit('updated', {
start: start,
end: end
});
}
if ($event) {
$event.preventDefault();
return false;
}
},
submitForm() {
// Allow Vue model to catch up to user input.
// Submitting form will cause validation messages to display (but only if triggered by button click)
this.$nextTick(() => this.setBoundsFromView());
},
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
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),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
//TODO: Do we need limits here? We have conductor limits disabled right now
// const limit = this.getBoundsLimit();
const limit = false;
if (this.timeSystem.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);
});
},
dismiss() {
this.$emit('dismiss');
areBoundsFormatsValid() {
let validationResult = {
valid: true
};
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
const formattedDate = input === this.$refs.startDate
? this.formattedBounds.start
: this.formattedBounds.end
;
if (!this.timeFormatter.validate(formattedDate)) {
validationResult = {
valid: false,
message: 'Invalid date'
};
}
return this.handleValidationResults(input, validationResult);
});
},
getBoundsLimit() {
const configuration = this.configuration.menuOptions
.filter(option => option.timeSystem === this.timeSystem.key)
.find(option => option.limit);
const limit = configuration ? configuration.limit : undefined;
return limit;
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
} else {
input.setCustomValidity('');
input.title = '';
}
this.$refs.fixedDeltaInput.reportValidity();
return validationResult.valid;
},
startDateSelected(date) {
this.formattedBounds.start = this.timeFormatter.format(date);
this.validateAllBounds('startDate');
this.submitForm();
},
endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date);
this.validateAllBounds('endDate');
this.submitForm();
}
}
};

View File

@@ -1,43 +1,95 @@
<template>
<time-popup-realtime
v-if="readOnly === false"
:offsets="offsets"
@focus.native="$event.target.select()"
@update="timePopUpdate"
@dismiss="dismiss"
/>
<div
v-else
class="c-compact-tc__bounds"
<form
ref="deltaInput"
class="c-conductor__inputs"
>
<div class="c-compact-tc__bounds__value icon-minus">{{ offsets.start }}</div>
<div
v-if="compact"
class="c-compact-tc__bounds__start-end-sep icon-arrows-right-left"
></div>
<div
v-else
class="c-compact-tc__current-update"
class="c-ctrl-wrapper c-conductor-input c-conductor__start-delta"
>
LAST UPDATE {{ formattedBounds.end }}
<!-- RT start -->
<div class="c-direction-indicator icon-minus"></div>
<time-popup
v-if="showTCInputStart"
class="pr-tc-input-menu--start"
:bottom="keyString !== undefined"
:type="'start'"
:offset="offsets.start"
@focus.native="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>
<button
ref="startOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset after now"
data-testid="conductor-start-offset-button"
@click.prevent.stop="showTimePopupStart"
>
{{ offsets.start }}
</button>
</div>
<div class="c-compact-tc__bounds__value icon-plus">{{ offsets.end }}</div>
</div>
<div class="c-ctrl-wrapper c-conductor-input c-conductor__end-fixed">
<!-- RT 'last update' display -->
<div class="c-conductor__end-fixed__label">
Current
</div>
<input
ref="endDate"
v-model="formattedCurrentValue"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
:disabled="true"
>
</div>
<div
class="c-ctrl-wrapper c-conductor-input c-conductor__end-delta"
>
<!-- RT end -->
<div class="c-direction-indicator icon-plus"></div>
<time-popup
v-if="showTCInputEnd"
class="pr-tc-input-menu--end"
:bottom="keyString !== undefined"
:type="'end'"
:offset="offsets.end"
@focus.native="$event.target.select()"
@hide="hideAllTimePopups"
@update="timePopUpdate"
/>
<button
ref="endOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset preceding now"
data-testid="conductor-end-offset-button"
@click.prevent.stop="showTimePopupEnd"
>
{{ offsets.end }}
</button>
</div>
</form>
</template>
<script>
import TimePopupRealtime from "./timePopupRealtime.vue";
import timePopup from "./timePopup.vue";
import _ from "lodash";
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
TimePopupRealtime
timePopup
},
inject: ['openmct'],
props: {
keyString: {
type: String,
default() {
return undefined;
}
},
objectPath: {
type: Array,
default() {
@@ -49,18 +101,6 @@ export default {
default() {
return undefined;
}
},
readOnly: {
type: Boolean,
default() {
return false;
}
},
compact: {
type: Boolean,
default() {
return false;
}
}
},
data() {
@@ -94,15 +134,8 @@ export default {
};
},
watch: {
objectPath: {
handler(newPath, oldPath) {
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
keyString() {
this.setTimeContext();
},
inputBounds: {
handler(newBounds) {
@@ -126,17 +159,19 @@ export default {
this.handleNewBounds(this.timeContext.bounds());
this.setViewFromOffsets(this.timeContext.clockOffsets());
this.timeContext.on('bounds', this.handleNewBounds);
this.timeContext.on('clock', this.clearAllValidation);
this.timeContext.on('clockOffsets', this.setViewFromOffsets);
},
stopFollowingTime() {
if (this.timeContext) {
this.timeContext.off('bounds', this.handleNewBounds);
this.timeContext.off('clock', this.clearAllValidation);
this.timeContext.off('clockOffsets', this.setViewFromOffsets);
}
},
setTimeContext() {
this.stopFollowingTime();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext = this.openmct.time.getContextForView(this.keyString ? this.objectPath : []);
this.followTime();
},
handleNewBounds(bounds) {
@@ -144,6 +179,13 @@ export default {
this.setViewFromBounds(bounds);
this.updateCurrentValue();
},
clearAllValidation() {
[this.$refs.startOffset, this.$refs.endOffset].forEach(this.clearValidationForInput);
},
clearValidationForInput(input) {
input.setCustomValidity('');
input.title = '';
},
setViewFromOffsets(offsets) {
if (offsets) {
this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start));
@@ -180,22 +222,86 @@ export default {
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();
hideAllTimePopups() {
this.showTCInputStart = false;
this.showTCInputEnd = false;
},
setOffsetsFromView() {
let startOffset = 0 - this.durationFormatter.parse(this.offsets.start);
let endOffset = this.durationFormatter.parse(this.offsets.end);
showTimePopupStart() {
this.hideAllTimePopups();
this.showTCInputStart = !this.showTCInputStart;
},
showTimePopupEnd() {
this.hideAllTimePopups();
this.showTCInputEnd = !this.showTCInputEnd;
},
timePopUpdate({ type, hours, minutes, seconds }) {
this.offsets[type] = [hours, minutes, seconds].join(':');
this.setOffsetsFromView();
this.hideAllTimePopups();
},
setOffsetsFromView($event) {
if (this.$refs.deltaInput.checkValidity()) {
let startOffset = 0 - this.durationFormatter.parse(this.offsets.start);
let endOffset = this.durationFormatter.parse(this.offsets.end);
this.$emit('updated', {
start: startOffset,
end: endOffset
this.$emit('updated', {
start: startOffset,
end: endOffset
});
}
if ($event) {
$event.preventDefault();
return false;
}
},
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
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),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
//TODO: Do we need limits here? We have conductor limits disabled right now
// const limit = this.getBoundsLimit();
const limit = false;
if (this.timeSystem.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);
});
},
dismiss() {
this.$emit('dismiss');
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
} else {
input.setCustomValidity('');
input.title = '';
}
return validationResult.valid;
}
}
};

View File

@@ -22,15 +22,11 @@
<template>
<div
ref="modeButton"
class="c-tc-input-popup__options"
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
>
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-button--menu c-button--compact js-mode-button"
:class="[
buttonCssClass,
selectedMode.cssClass
]"
class="c-button--menu c-mode-button"
@click.prevent.stop="showModesMenu"
>
<span class="c-button__label">{{ selectedMode.name }}</span>
@@ -45,15 +41,6 @@ import toggleMixin from '../../ui/mixins/toggle-mixin';
export default {
mixins: [toggleMixin],
inject: ['openmct', 'configuration'],
props: {
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
}
},
data: function () {
let activeClock = this.openmct.time.clock();
if (activeClock !== undefined) {
@@ -147,10 +134,6 @@ export default {
clockKey = undefined;
}
let option = {
clockKey
};
let configuration = this.getMatchingConfig({
clock: clockKey,
timeSystem: this.openmct.time.timeSystem().key
@@ -160,19 +143,16 @@ export default {
configuration = this.getMatchingConfig({
clock: clockKey
});
option.timeSystem = configuration.timeSystem;
option.bounds = configuration.bounds;
this.openmct.time.timeSystem(configuration.timeSystem, configuration.bounds);
}
if (clockKey === undefined) {
// this.openmct.time.stopClock();
this.openmct.time.stopClock();
} else {
const offsets = this.openmct.time.clockOffsets() || configuration.clockOffsets;
option.offsets = offsets;
// this.openmct.time.clock(clockKey, offsets);
this.openmct.time.clock(clockKey, offsets);
}
this.$emit('updated', option);
},
getMatchingConfig(options) {

View File

@@ -21,17 +21,6 @@
*****************************************************************************/
<template>
<div class="c-clock-symbol">
<svg
class="c-clock-symbol__outer"
viewBox="0 0 16 16"
>
<path
d="M6 0L3 0C1.34315 0 0 1.34315 0 3V13C0 14.6569 1.34315 16 3 16H6V13H3V3H6V0Z"
/>
<path
d="M10 13H13V3H10V0H13C14.6569 0 16 1.34315 16 3V13C16 14.6569 14.6569 16 13 16H10V13Z"
/>
</svg>
<div class="hand-little"></div>
<div class="hand-big"></div>
</div>

View File

@@ -1,210 +0,0 @@
<template>
<div
:style="position"
class="c-tc-input-popup"
:class="modeClass"
@click.stop
@keydown.enter.prevent
@keyup.enter.prevent="submit"
@keydown.esc.prevent
@keyup.esc.prevent="hide"
>
<div
class="c-tc-input-popup__options"
>
<Mode
v-if="isIndependent"
class="c-button--compact c-conductor__mode-select"
:mode="timeOptionMode"
:button-css-class="'c-button--compact'"
@modeChanged="saveIndependentMode"
/>
<ConductorMode
v-else
class="c-conductor__mode-select"
:button-css-class="'c-icon-button'"
@updated="saveMode"
/>
<!-- TODO: Time system and history must work even with ITC later -->
<ConductorTimeSystem
v-if="!isIndependent"
class="c-conductor__time-system-select"
:button-css-class="'c-icon-button'"
/>
<ConductorHistory
v-if="!isIndependent"
class="c-conductor__history-select"
:button-css-class="'c-icon-button'"
:offsets="timeOffsets"
:bounds="bounds"
:time-system="timeSystem"
:mode="timeMode"
/>
</div>
<conductor-inputs-fixed
v-if="isFixed"
:input-bounds="bounds"
:object-path="objectPath"
@updated="saveFixedBounds"
@dismiss="dismiss"
/>
<conductor-inputs-realtime
v-else
:input-bounds="bounds"
:object-path="objectPath"
@updated="saveClockOffsets"
@dismiss="dismiss"
/>
</div>
</template>
<script>
import ConductorMode from './ConductorMode.vue';
import Mode from './independent/Mode.vue';
import ConductorTimeSystem from "./ConductorTimeSystem.vue";
import ConductorHistory from "./ConductorHistory.vue";
import ConductorInputsFixed from "./ConductorInputsFixed.vue";
import ConductorInputsRealtime from "./ConductorInputsRealtime.vue";
export default {
components: {
ConductorMode,
Mode,
ConductorTimeSystem,
ConductorHistory,
ConductorInputsFixed,
ConductorInputsRealtime
},
inject: ['openmct', 'configuration'],
props: {
positionX: {
type: Number,
required: true
},
// positionY: {
// type: Number,
// required: true
// },
isIndependent: {
type: Boolean,
default() {
return false;
}
},
timeOptions: {
type: Object,
default() {
return undefined;
}
},
bottom: {
type: Boolean,
default() {
return false;
}
},
objectPath: {
type: Array,
default() {
return [];
}
}
},
data() {
let bounds = this.openmct.time.bounds();
let timeSystem = this.openmct.time.timeSystem();
return {
timeSystem: timeSystem,
bounds: {
start: bounds.start,
end: bounds.end
},
isFixed: this.openmct.time.clock() === undefined
};
},
computed: {
position() {
return {
left: `${this.positionX}px`
// top: `${this.positionY}px`
};
},
timeOffsets() {
return this.isFixed || !this.timeContext ? undefined : this.timeContext.clockOffsets();
},
timeMode() {
return this.isFixed ? 'fixed' : 'realtime';
},
modeClass() {
const value = this.bottom ? 'c-tc-input-popup--bottom' : '';
return this.isFixed ? `${value} c-tc-input-popup--fixed-mode` : `${value} c-tc-input-popup--realtime-mode`;
},
timeOptionMode() {
return this.timeOptions?.mode;
}
},
watch: {
objectPath: {
handler(newPath, oldPath) {
//domain object or view has probably changed
if (newPath === oldPath) {
return;
}
this.setTimeContext();
},
deep: true
}
},
mounted() {
this.setTimeContext();
},
beforeDestroy() {
this.stopFollowingTimeContext();
},
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('clock', this.setViewFromClock);
this.timeContext.on('bounds', this.setBounds);
this.setViewFromClock(this.timeContext.clock());
this.setBounds(this.timeContext.bounds());
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('clock', this.setViewFromClock);
this.timeContext.off('bounds', this.setBounds);
}
},
setViewFromClock(clock) {
this.isFixed = clock === undefined;
this.bounds = this.timeContext.bounds();
},
setBounds() {
this.bounds = this.timeContext.bounds();
},
saveFixedBounds(bounds) {
this.$emit('fixedBoundsUpdated', bounds);
},
saveClockOffsets(offsets) {
this.$emit('clockOffsetsUpdated', offsets);
},
saveMode(option) {
this.$emit('modeUpdated', option);
},
saveIndependentMode(mode) {
this.$emit('independentModeUpdated', mode);
},
dismiss() {
this.$emit('dismiss');
}
}
};
</script>

View File

@@ -26,13 +26,11 @@
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
>
<button
class="c-button--menu c-button--compact c-time-system-button"
:class="[
buttonCssClass
]"
class="c-button--menu c-time-system-button"
:class="selectedTimeSystem.cssClass"
@click.prevent.stop="showTimeSystemMenu"
>
{{ selectedTimeSystem.name }}
<span class="c-button__label">{{ selectedTimeSystem.name }}</span>
</button>
</div>
</template>
@@ -40,15 +38,6 @@
<script>
export default {
inject: ['openmct', 'configuration'],
props: {
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
}
},
data: function () {
let activeClock = this.openmct.time.clock();

View File

@@ -57,11 +57,11 @@
}
.is-realtime-mode & {
$c: 1px solid rgba($colorTimeRealtime, 0.7);
$c: 1px solid rgba($colorTime, 0.7);
border-left: $c;
border-right: $c;
svg text {
fill: $colorTimeRealtime;
fill: $colorTime;
}
}
}

View File

@@ -33,17 +33,22 @@
.c-clock-symbol {
$c: rgba($colorBodyFg, 0.5);
$d: 16px;
$c: $colorBtnBg; //$colorObjHdrIc;
$d: 18px;
height: $d;
width: $d;
position: relative;
&__outer {
// SVG brackets shape
&:before {
font-family: symbolsfont;
color: $c;
content: $glyph-icon-brackets;
font-size: $d;
line-height: normal;
display: block;
width: 100%;
height: 100%;
fill: $c;
z-index: 1;
}
// Clock hands
@@ -88,15 +93,14 @@
// Modes
.is-realtime-mode &,
.is-lad-mode & {
$c: $colorTimeRealtimeFgSubtle;
.c-clock-symbol__outer {
&:before {
// Brackets icon
fill: $c;
color: $colorTime;
}
div[class*="hand"] {
animation-name: clock-hands;
&:before {
background: $c;
background: $colorTime;
}
}
}

View File

@@ -1,9 +1,8 @@
.c-conductor__mode-menu {
max-height: 80vh;
max-width: 500px;
min-height: 50px;
//We don't need the z-index now that we're using the popup
//z-index: 70;
min-height: 250px;
z-index: 70;
[class*="__icon"] {
filter: $colorKeyFilter;

View File

@@ -9,15 +9,10 @@
/*********************************************** CONDUCTOR LAYOUT */
.c-conductor {
&__inputs {
display: flex;
flex: 0 0 auto;
> * + * {
margin-left: $interiorMargin;
}
display: contents;
}
/* &__time-bounds {
&__time-bounds {
display: grid;
grid-column-gap: $interiorMargin;
grid-row-gap: $interiorMargin;
@@ -44,17 +39,16 @@
grid-area: tc-end;
display: flex;
justify-content: flex-end;
}*/
}
&__ticks {
flex: 1 1 auto;
grid-area: tc-ticks;
}
&__controls {
grid-area: tc-controls;
display: flex;
align-items: center;
> * + * {
margin-left: $interiorMargin;
}
@@ -113,8 +107,7 @@
background: rgba($timeConductorActiveBg, 0.4);
border-left-color: $timeConductorActiveBg;
border-right-color: $timeConductorActiveBg;
top: 0;
bottom: 0;
top: 0; bottom: 0;
}
}
}
@@ -130,7 +123,7 @@
}
}
/* body.phone.portrait & {
body.phone.portrait & {
.c-conductor__time-bounds {
grid-row-gap: $interiorMargin;
grid-template-rows: auto auto;
@@ -167,8 +160,8 @@
grid-template-areas:
"tc-mode-icon tc-start tc-start"
"tc-mode-icon tc-end tc-end"
}
}
}
&.is-realtime-mode {
.c-conductor__time-bounds {
@@ -181,20 +174,21 @@
justify-content: flex-end;
}
}
}*/
}
}
.c-conductor-holder--compact {
//min-height: 22px;
flex: 0 1 auto;
overflow: hidden;
min-height: 22px;
.c-conductor {
&__inputs,
&__time-bounds {
display: flex;
flex: 0 1 auto;
overflow: hidden;
.c-toggle-switch {
// Used in independent Time Conductor
flex: 0 0 auto;
}
}
&__inputs {
@@ -224,32 +218,38 @@
margin-right: $interiorMarginSm;
}
.c-direction-indicator {
// Holds realtime-mode + and - symbols
font-size: 0.7em;
}
input:invalid {
background: rgba($colorFormInvalid, 0.5);
}
}
.is-realtime-mode {
.c-conductor__controls button,
.c-conductor__delta-button {
//@include themedButton($colorTimeRealtimeBg);
color: $colorTimeRealtimeFg;
@include themedButton($colorTimeBg);
color: $colorTimeFg;
}
.c-conductor-input {
&:before {
color: $colorTimeRealtimeFgSubtle;
color: $colorTime;
}
}
.c-conductor__end-fixed {
// Displays last RT update
color: $colorTimeRealtimeFgSubtle;
// Displays last RT udpate
color: $colorTime;
input {
// Remove input look
background: none;
box-shadow: none;
color: $colorTimeRealtimeFgSubtle;
color: $colorTime;
pointer-events: none;
&[disabled] {
@@ -259,7 +259,6 @@
}
}
//TODO: Do we need this?
[class^='pr-tc-input-menu'] {
// Uses ^= here to target both start and end menus
background: $colorBodyBg;
@@ -282,250 +281,30 @@
}
}
.l-shell__time-conductor .c-tc-input-popup--end {
.l-shell__time-conductor .pr-tc-input-menu--end {
left: auto;
right: 0;
}
.pr-time-label {
font-size: 0.9em;
text-transform: uppercase;
&:before {
[class^='pr-time'] {
&[class*='label'] {
font-size: 0.8em;
margin-right: $interiorMarginSm;
}
}
.pr-time-input {
display: flex;
align-items: center;
white-space: nowrap;
> * + * {
margin-left: $interiorMarginSm;
opacity: 0.6;
text-transform: uppercase;
}
input {
height: 22px;
line-height: 1em;
font-size: 1.25em;
}
&--date input {
width: 120px;
}
&--time input {
width: 70px;
}
&--buttons {
> * + * {
margin-left: $interiorMargin;
}
}
&__start-end-sep {
height: 100%;
}
&--input-and-button {
@include wrappedInput();
}
}
/*********************************************** COMPACT TIME CONDUCTOR */
.c-compact-tc,
.c-tc-input-popup {
[class*='start-end-sep'] {
opacity: 0.5;
}
}
.c-compact-tc {
border-radius: $controlCr;
display: flex;
flex-direction: row;
flex: 0 1 auto;
overflow: hidden;
align-items: center;
padding: 2px $interiorMarginSm;
> * + * {
margin-left: $interiorMargin;
}
&__bounds,
&__bounds__value {
&[class*='controls'] {
display: flex;
align-items: center;
white-space: nowrap;
> * + * {
margin-left: $interiorMargin;
}
}
&__bounds {
cursor: pointer;
flex: 0 1 auto;
overflow: hidden;
> * + * {
flex: 0 0 auto;
}
}
&__bounds__value {
@include ellipsize();
color: $colorTimeRealtimeFg;
flex: 0 1 auto;
&:before {
font-size: 0.85em;
input {
height: 22px;
line-height: 22px;
margin-right: $interiorMarginSm;
}
}
&__current-update {
@include ellipsize();
flex: 0 1 auto;
}
.c-direction-indicator {
// Holds realtime-mode + and - symbols
font-size: 0.7em;
}
.c-toggle-switch,
.c-clock-symbol {
// Used in independent Time Conductor
flex: 0 0 auto;
}
.c-so-view & {
// Time Conductor in a Layout frame
.c-clock-symbol {
$h: 14px;
height: $h;
width: $h;
}
[class*='button'] {
$p: 0px;
padding: $p $p + 1;
font-size: 1.25em;
width: 42px;
}
}
}
.is-fixed-mode.is-expanded {
&.c-compact-tc,
.c-tc-input-popup {
background: $colorTimeFixedBg;
color: $colorTimeFixedFgSubtle;
em,
.pr-time-label:before {
color: $colorTimeFixedFg;
}
&__bounds__valuelue {
color: $colorTimeFixedFg;
}
&__time-value {
color: $colorTimeFixedFg;
}
[class*='c-button'] {
background: $colorTimeFixedBtnBg;
color: $colorTimeFixedBtnFg;
[class*='label'] {
color: $colorTimeRealtimeFg;
}
}
}
}
.is-realtime-mode.is-expanded {
&.c-compact-tc,
.c-tc-input-popup {
background: rgba($colorTimeRealtimeBg, 1);
color: $colorTimeRealtimeFgSubtle;
em,
.pr-time-label:before {
color: $colorTimeRealtimeFg;
}
&__bounds__valuelue {
color: $colorTimeRealtimeFg;
}
&__time-value {
color: $colorTimeRealtimeFg;
}
[class*='c-button'] {
background: $colorTimeRealtimeBtnBg;
color: $colorTimeRealtimeBtnFg;
[class*='label'] {
color: $colorTimeRealtimeFg;
}
}
}
}
.c-compact-tc {
&.l-shell__time-conductor {
// Main view
min-height: 24px;
}
}
/*********************************************** INPUTS POPUP DIALOG */
.c-tc-input-popup {
@include menuOuter();
padding: $interiorMarginLg;
position: absolute;
width: min-content;
bottom: 35px;
> * + * {
margin-top: $interiorMarginLg;
}
&[class*='--bottom'] {
bottom: auto;
top: 35px;
}
&__options {
display: flex;
> * + * {
margin-left: $interiorMargin;
}
}
&--fixed-mode {
.c-tc-input-popup__input-grid {
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 2fr;
}
}
&--realtime-mode {
.c-tc-input-popup__input-grid {
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 2fr;
}
}
&__input-grid {
display: grid;
grid-column-gap: 3px;
grid-row-gap: $interiorMargin;
align-items: start;
}
}

View File

@@ -1,136 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, 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 raf from '@/utils/raf';
import Vue from "vue";
import ConductorPopUp from "./ConductorPopUp.vue";
export default {
inject: ['openmct', 'configuration'],
mounted() {
this.showPopup = this.showPopup.bind(this);
this.clearPopup = this.clearPopup.bind(this);
this.positionBox = this.positionBox.bind(this);
this.positionBox = raf(this.positionBox);
this.timeConductorOptionsHolder = this.$refs.timeConductorOptionsHolder;
this.registerPopUp();
this.popupComponent = this.createPopupComponent();
},
beforeDestroy() {
this.removePopup();
},
methods: {
showPopup() {
const popupElement = this.popupComponent;
document.body.appendChild(popupElement.$el);
//Use capture, so we don't trigger immediately on the same iteration of the event loop
document.addEventListener('click', this.clearPopup, {
capture: true
});
this.positionBox();
window.addEventListener('resize', this.positionBox);
},
positionBox() {
const popupElement = this.popupComponent;
const timeConductorOptions = this.timeConductorOptionsHolder;
let timeConductorOptionsBox = timeConductorOptions.getBoundingClientRect();
popupElement.positionX = timeConductorOptionsBox.left;
//TODO: PositionY should be calculated to be top or bottom based on the location of the conductor options
popupElement.positionY = timeConductorOptionsBox.top;
const offsetTop = popupElement.$el.getBoundingClientRect().height;
const popupRight = popupElement.positionX + popupElement.$el.clientWidth;
const offsetLeft = Math.min(window.innerWidth - popupRight, 0);
popupElement.positionX = popupElement.positionX + offsetLeft;
popupElement.positionY = popupElement.positionY - offsetTop;
},
clearPopup(clickAwayEvent) {
if (this.canClose(clickAwayEvent)) {
clickAwayEvent.stopPropagation();
this.removePopup();
}
},
canClose(clickAwayEvent) {
const popupElement = this.popupComponent;
const isChildMenu = clickAwayEvent.target.closest('.c-menu') !== null;
const isPopupElementItem = popupElement.$el.contains(clickAwayEvent.target);
return !isChildMenu && !isPopupElementItem;
},
removePopup() {
const popupElement = this.popupComponent;
popupElement.$el.remove();
document.removeEventListener('click', this.clearPopup, {
capture: true
});
window.removeEventListener('resize', this.positionBox);
},
createPopupComponent() {
const saveFixedBounds = this.saveFixedBounds;
const saveClockOffsets = this.saveClockOffsets;
const saveMode = this.saveMode;
const removePopup = this.removePopup;
const popupElement = new Vue({
components: {
ConductorPopUp
},
provide: {
openmct: this.openmct,
configuration: this.configuration
},
data() {
return {
positionX: 0,
positionY: 0,
saveClockOffsets,
saveFixedBounds,
saveMode,
removePopup
};
},
template: `<conductor-pop-up
@dismiss="removePopup()"
@modeUpdated="saveMode"
@fixedBoundsUpdated="saveFixedBounds"
@clockOffsetsUpdated="saveClockOffsets"
:bottom="false"
:positionX="positionX"
:positionY="positionY" />`
}).$mount();
return popupElement;
},
registerPopUp() {
this.timeConductorOptionsHolder.addEventListener('click', this.showPopup);
}
}
};

View File

@@ -21,39 +21,49 @@
*****************************************************************************/
<template>
<div
ref="timeConductorOptionsHolder"
class="c-compact-tc"
class="c-conductor"
:class="[
isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode',
{ 'is-expanded' : independentTCEnabled }
isFixed ? 'is-fixed-mode' : independentTCEnabled ? 'is-realtime-mode' : 'is-fixed-mode'
]"
>
<toggle-switch
id="independentTCToggle"
class="c-toggle-switch--mini"
:checked="independentTCEnabled"
:title="`${independentTCEnabled ? 'Disable' : 'Enable'} independent Time Conductor`"
@change="toggleIndependentTC"
/>
<div class="c-conductor__time-bounds">
<toggle-switch
id="independentTCToggle"
:checked="independentTCEnabled"
:title="`${independentTCEnabled ? 'Disable' : 'Enable'} independent Time Conductor`"
@change="toggleIndependentTC"
/>
<ConductorModeIcon v-if="independentTCEnabled" />
<ConductorModeIcon />
<conductor-inputs-fixed
v-if="isFixed && independentTCEnabled"
class="c-compact-tc__bounds--fixed"
:object-path="objectPath"
:read-only="true"
:compact="true"
/>
<div
v-if="timeOptions && independentTCEnabled"
class="c-conductor__controls"
>
<Mode
v-if="mode"
class="c-conductor__mode-select"
:key-string="domainObject.identifier.key"
:mode="timeOptions.mode"
:enabled="independentTCEnabled"
@modeChanged="saveMode"
/>
<conductor-inputs-realtime
v-if="!isFixed && independentTCEnabled"
class="c-compact-tc__bounds--real-time"
:object-path="objectPath"
:read-only="true"
:compact="true"
/>
<div class="c-not-button c-not-button--compact c-compact-tc__gear icon-gear"></div>
<conductor-inputs-fixed
v-if="isFixed"
:key-string="domainObject.identifier.key"
:object-path="objectPath"
@updated="saveFixedOffsets"
/>
<conductor-inputs-realtime
v-else
:key-string="domainObject.identifier.key"
:object-path="objectPath"
@updated="saveClockOffsets"
/>
</div>
</div>
</div>
</template>
@@ -62,16 +72,16 @@ import ConductorInputsFixed from "../ConductorInputsFixed.vue";
import ConductorInputsRealtime from "../ConductorInputsRealtime.vue";
import ConductorModeIcon from "@/plugins/timeConductor/ConductorModeIcon.vue";
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import independentTimeConductorPopUpManager from "./independentTimeConductorPopUpManager";
import Mode from "./Mode.vue";
export default {
components: {
Mode,
ConductorModeIcon,
ConductorInputsRealtime,
ConductorInputsFixed,
ToggleSwitch
},
mixins: [independentTimeConductorPopUpManager],
inject: ['openmct'],
props: {
domainObject: {
@@ -84,19 +94,13 @@ export default {
}
},
data() {
const bounds = this.openmct.time.bounds();
return {
timeOptions: this.domainObject.configuration.timeOptions || {
clockOffsets: this.openmct.time.clockOffsets(),
fixedOffsets: this.openmct.time.bounds()
},
mode: undefined,
independentTCEnabled: this.domainObject.configuration.useIndependentTime === true,
viewBounds: {
start: bounds.start,
end: bounds.end
}
independentTCEnabled: this.domainObject.configuration.useIndependentTime === true
};
},
computed: {
@@ -126,13 +130,6 @@ export default {
}
},
deep: true
},
objectPath: {
handler() {
//domain object or view has probably changed
this.setTimeContext();
},
deep: true
}
},
mounted() {
@@ -166,7 +163,6 @@ export default {
if (this.independentTCEnabled) {
this.registerIndependentTimeOffsets();
} else {
this.removePopup();
this.destroyIndependentTime();
}
@@ -191,10 +187,11 @@ export default {
this.registerIndependentTimeOffsets();
}
},
saveFixedBounds(bounds) {
saveFixedOffsets(offsets) {
const newOptions = Object.assign({}, this.timeOptions, {
fixedOffsets: bounds
fixedOffsets: offsets
});
this.updateTimeOptions(newOptions);
},
saveClockOffsets(offsets) {

View File

@@ -28,11 +28,7 @@
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
v-if="selectedMode"
class="c-icon-button c-button--menu js-mode-button"
:class="[
buttonCssClass,
selectedMode.cssClass
]"
class="c-button--menu c-mode-button"
@click.prevent.stop="showModesMenu"
>
<span class="c-button__label">{{ selectedMode.name }}</span>
@@ -59,13 +55,6 @@ export default {
default() {
return false;
}
},
buttonCssClass: {
type: String,
required: false,
default() {
return '';
}
}
},
data: function () {

View File

@@ -1,147 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, 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 raf from '@/utils/raf';
import Vue from "vue";
import ConductorPopUp from "../ConductorPopUp.vue";
export default {
inject: ['openmct'],
mounted() {
this.showPopup = this.showPopup.bind(this);
this.clearPopup = this.clearPopup.bind(this);
this.positionBox = this.positionBox.bind(this);
this.positionBox = raf(this.positionBox);
this.timeConductorOptionsHolder = this.$refs.timeConductorOptionsHolder;
this.registerPopUp();
this.popupComponent = this.createPopupComponent();
},
beforeDestroy() {
this.removePopup();
},
methods: {
showPopup() {
if (!this.independentTCEnabled) {
return;
}
const popupElement = this.popupComponent;
document.body.appendChild(popupElement.$el);
//Use capture, so we don't trigger immediately on the same iteration of the event loop
document.addEventListener('click', this.clearPopup, {
capture: true
});
this.positionBox();
window.addEventListener('resize', this.positionBox);
},
positionBox() {
const popupElement = this.popupComponent;
const timeConductorOptions = this.timeConductorOptionsHolder;
let timeConductorOptionsBox = timeConductorOptions.getBoundingClientRect();
popupElement.positionX = timeConductorOptionsBox.left;
//TODO: PositionY should be calculated to be top or bottom based on the location of the conductor options
popupElement.positionY = timeConductorOptionsBox.top;
const offsetTop = popupElement.$el.getBoundingClientRect().height;
const popupRight = popupElement.positionX + popupElement.$el.clientWidth;
const offsetLeft = Math.min(window.innerWidth - popupRight, 0);
popupElement.positionX = popupElement.positionX + offsetLeft;
popupElement.positionY = popupElement.positionY - offsetTop;
},
clearPopup(clickAwayEvent) {
if (this.canClose(clickAwayEvent)) {
clickAwayEvent.stopPropagation();
this.removePopup();
}
},
canClose(clickAwayEvent) {
const popupElement = this.popupComponent;
const isChildMenu = clickAwayEvent.target.closest('.c-menu') !== null;
const isPopupElementItem = popupElement.$el.contains(clickAwayEvent.target);
return !isChildMenu && !isPopupElementItem;
},
removePopup() {
const popupElement = this.popupComponent;
document.removeEventListener('click', this.clearPopup, {
capture: true
});
window.removeEventListener('resize', this.positionBox);
popupElement.$el.remove();
},
createPopupComponent() {
const saveFixedBounds = this.saveFixedBounds;
const saveClockOffsets = this.saveClockOffsets;
const saveMode = this.saveMode;
const removePopup = this.removePopup;
const objectPath = this.objectPath;
const timeOptions = this.timeOptions;
const popupElement = new Vue({
components: {
ConductorPopUp
},
provide: {
openmct: this.openmct,
configuration: undefined
},
data() {
return {
positionX: 0,
positionY: 0,
saveClockOffsets,
saveFixedBounds,
saveMode,
removePopup,
timeOptions,
objectPath
};
},
template: `<conductor-pop-up
@dismiss="removePopup()"
@independentModeUpdated="saveMode"
@fixedBoundsUpdated="saveFixedBounds"
@clockOffsetsUpdated="saveClockOffsets"
:object-path="objectPath"
:is-independent="true"
:time-options="timeOptions"
:bottom="true"
:positionX="positionX"
:positionY="positionY" />`
}).$mount();
return popupElement;
},
registerPopUp() {
this.timeConductorOptionsHolder.addEventListener('click', this.showPopup);
}
}
};

View File

@@ -106,7 +106,7 @@ describe('time conductor', () => {
describe('in realtime mode', () => {
beforeEach((done) => {
const switcher = appHolder.querySelector('.js-mode-button');
const switcher = appHolder.querySelector('.c-mode-button');
const clickEvent = createMouseEvent("click");
switcher.dispatchEvent(clickEvent);

View File

@@ -1,160 +1,96 @@
<template>
<div
class="c-tc-input-popup"
:class="{'c-tc-input-popup--bottom' : bottom === true}"
class="pr-tc-input-menu"
:class="{'pr-tc-input-menu--bottom' : bottom === true}"
@keydown.enter.prevent
@keyup.enter.prevent="submit"
@keydown.esc.prevent
@keyup.esc.prevent="hide"
@click.stop
>
<slot></slot>
<div
v-if="false"
>
<div class="c-tc-input-popup__options c-tc-input-popup__options--real-time">
Buttons here
</div>
<div class="pr-time-label__hrs">Hrs</div>
<div class="pr-time-label__mins">Mins</div>
<div class="pr-time-label__secs">Secs</div>
<div class="c-tc-input-popup__input-grid">
<div class="pr-time-label icon-line-horz">Hrs</div>
<div class="pr-time-label">Mins</div>
<div class="pr-time-label">Secs</div>
<div class="pr-time-label"></div>
<div class="pr-time-label icon-plus">Hrs</div>
<div class="pr-time-label">Mins</div>
<div class="pr-time-label">Secs</div>
<div class="pr-time-label"></div>
<div class="pr-time-input">
<input
ref="inputHrs"
v-model="inputHrs"
class="pr-time-input__hrs"
step="1"
type="number"
min="0"
max="23"
title="Enter 0 - 23"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('inputHrs')"
@wheel="increment($event, 'inputHrs')"
>
:
</div>
<div class="pr-time-input">
<input
ref="inputMins"
v-model="inputMins"
type="number"
class="pr-time-input__mins"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('inputMins')"
@wheel="increment($event, 'inputMins')"
>
:
</div>
<div class="pr-time-input">
<input
ref="inputSecs"
v-model="inputSecs"
type="number"
class="pr-time-input__secs"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('inputSecs')"
@wheel="increment($event, 'inputSecs')"
>
</div>
<div class="pr-time-input pr-time-input__start-end-sep icon-arrows-right-left"></div>
<div class="pr-time-input">
<input
ref="inputHrs"
v-model="inputHrs"
class="pr-time-input__hrs"
step="1"
type="number"
min="0"
max="23"
title="Enter 0 - 23"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('inputHrs')"
@wheel="increment($event, 'inputHrs')"
>
:
</div>
<div class="pr-time-input">
<input
ref="inputMins"
v-model="inputMins"
type="number"
class="pr-time-input__mins"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('inputMins')"
@wheel="increment($event, 'inputMins')"
>
:
</div>
<div class="pr-time-input pr-time-input--buttons">
<input
ref="inputSecs"
v-model="inputSecs"
type="number"
class="pr-time-input__secs"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('inputSecs')"
@wheel="increment($event, 'inputSecs')"
>
</div>
<div class="pr-time-input pr-time-input--buttons">
<button
class="c-button c-button--major icon-check"
:disabled="isDisabled"
@click.prevent="submit"
></button>
<button
class="c-button icon-x"
@click.prevent="hide"
></button>
</div>
<div class="pr-time-controls">
<input
ref="inputHrs"
v-model="inputHrs"
class="pr-time-controls__hrs"
step="1"
type="number"
min="0"
max="23"
title="Enter 0 - 23"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('inputHrs')"
@wheel="increment($event, 'inputHrs')"
>
:
</div>
<div class="pr-time-controls">
<input
ref="inputMins"
v-model="inputMins"
type="number"
class="pr-time-controls__mins"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('inputMins')"
@wheel="increment($event, 'inputMins')"
>
:
</div>
<div class="pr-time-controls">
<input
ref="inputSecs"
v-model="inputSecs"
type="number"
class="pr-time-controls__secs"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('inputSecs')"
@wheel="increment($event, 'inputSecs')"
>
<div class="pr-time__buttons">
<button
class="c-button c-button--major icon-check"
:disabled="isDisabled"
@click.prevent="submit"
></button>
<button
class="c-button icon-x"
@click.prevent="hide"
></button>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
type: {
type: String,
required: true
},
offset: {
type: String,
required: true
},
bottom: {
type: Boolean,
default() {
@@ -170,11 +106,6 @@ export default {
isDisabled: false
};
},
computed: {
isRealtime() {
return this.mode.indexOf('realtime') !== -1;
}
},
mounted() {
this.setOffset();
document.addEventListener('click', this.hide);

View File

@@ -1,314 +0,0 @@
<template>
<form
ref="fixedDeltaInput"
class="c-tc-input-popup__input-grid"
>
<div class="pr-time-label"><em>Start</em> Date</div>
<div class="pr-time-label">Time Z</div>
<div class="pr-time-label"></div>
<div class="pr-time-label"><em>End</em> Date</div>
<div class="pr-time-label">Time Z</div>
<div class="pr-time-label"></div>
<div class="pr-time-input pr-time-input--date pr-time-input--input-and-button">
<input
ref="startDate"
v-model="formattedBounds.start"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('startDate'); submitForm()"
>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:default-date-time="formattedBounds.start"
:formatter="timeFormatter"
@date-selected="startDateSelected"
/>
</div>
<div class="pr-time-input pr-time-input--time">
<input
ref="startTime"
v-model="formattedBounds.startTime"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('startDate'); submitForm()"
>
</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">
<input
ref="endDate"
v-model="formattedBounds.end"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('endDate'); submitForm()"
>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
:default-date-time="formattedBounds.end"
:formatter="timeFormatter"
@date-selected="endDateSelected"
/>
</div>
<div class="pr-time-input pr-time-input--time">
<input
ref="endTime"
v-model="formattedBounds.endTime"
class="c-input--datetime"
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds('endDate'); submitForm()"
>
</div>
<div class="pr-time-input pr-time-input--buttons">
<button
class="c-button c-button--major icon-check"
:disabled="isDisabled"
@click.prevent="submit"
></button>
<button
class="c-button icon-x"
@click.prevent="hide"
></button>
</div>
</form>
</template>
<script>
import _ from "lodash";
import DatePicker from "./DatePicker.vue";
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
components: {
DatePicker
},
inject: ['openmct'],
props: {
inputBounds: {
type: Object,
required: true
},
inputTimeSystem: {
type: Object,
required: true
}
},
data() {
let timeSystem = this.openmct.time.timeSystem();
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
let timeFormatter = this.getFormatter(timeSystem.timeFormat);
let bounds = this.bounds || this.openmct.time.bounds();
return {
timeFormatter,
durationFormatter,
bounds: {
start: bounds.start,
end: bounds.end
},
formattedBounds: {
start: timeFormatter.format(bounds.start).split(' ')[0],
end: timeFormatter.format(bounds.end).split(' ')[0],
startTime: durationFormatter.format(Math.abs(bounds.start)),
endTime: durationFormatter.format(Math.abs(bounds.end))
},
isUTCBased: timeSystem.isUTCBased,
isDisabled: false
};
},
watch: {
inputBounds: {
handler(newBounds) {
this.handleNewBounds(newBounds);
},
deep: true
},
inputTimeSystem: {
handler(newTimeSystem) {
this.setTimeSystem(newTimeSystem);
},
deep: true
}
},
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
},
beforeDestroy() {
this.clearAllValidation();
},
methods: {
handleNewBounds(bounds) {
this.setBounds(bounds);
this.setViewFromBounds(bounds);
},
clearAllValidation() {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
},
clearValidationForInput(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.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;
},
setBoundsFromView(dismiss) {
if (this.$refs.fixedDeltaInput.checkValidity()) {
let start = this.timeFormatter.parse(`${this.formattedBounds.start} ${this.formattedBounds.startTime}`);
let end = this.timeFormatter.parse(`${this.formattedBounds.end} ${this.formattedBounds.endTime}`);
this.$emit('update', {
start: start,
end: end
});
}
if (dismiss) {
this.$emit('dismiss');
return false;
}
},
submit() {
this.validateAllBounds('startDate');
this.validateAllBounds('endDate');
this.submitForm(!this.isDisabled);
},
submitForm(dismiss) {
// Allow Vue model to catch up to user input.
// Submitting form will cause validation messages to display (but only if triggered by button click)
this.$nextTick(() => this.setBoundsFromView(dismiss));
},
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
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.timeSystem.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() {
let validationResult = {
valid: true
};
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}`
;
if (!this.timeFormatter.validate(formattedDate)) {
validationResult = {
valid: false,
message: 'Invalid date'
};
}
return this.handleValidationResults(input, validationResult);
});
},
getBoundsLimit() {
const configuration = this.configuration.menuOptions
.filter(option => option.timeSystem === this.timeSystem.key)
.find(option => option.limit);
const limit = configuration ? configuration.limit : undefined;
return limit;
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message);
input.title = validationResult.message;
this.isDisabled = true;
} else {
input.setCustomValidity('');
input.title = '';
this.isDisabled = false;
}
this.$refs.fixedDeltaInput.reportValidity();
return validationResult.valid;
},
startDateSelected(date) {
this.formattedBounds.start = this.timeFormatter.format(date).split(' ')[0];
this.validateAllBounds('startDate');
this.submitForm();
},
endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date).split(' ')[0];
this.validateAllBounds('endDate');
this.submitForm();
},
hide($event) {
if ($event.target.className.indexOf('c-button icon-x') > -1) {
this.$emit('dismiss');
}
}
}
};
</script>

View File

@@ -1,252 +0,0 @@
<template>
<form
ref="deltaInput"
class="c-tc-input-popup__input-grid"
>
<div class="pr-time-label icon-minus">Hrs</div>
<div class="pr-time-label">Mins</div>
<div class="pr-time-label">Secs</div>
<div class="pr-time-label"></div>
<div class="pr-time-label icon-plus">Hrs</div>
<div class="pr-time-label">Mins</div>
<div class="pr-time-label">Secs</div>
<div class="pr-time-label"></div>
<div class="pr-time-input">
<input
ref="startInputHrs"
v-model="startInputHrs"
class="pr-time-input__hrs"
step="1"
type="number"
min="0"
max="23"
title="Enter 0 - 23"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('startInputHrs')"
@wheel="increment($event, 'startInputHrs')"
>
<b>:</b>
</div>
<div class="pr-time-input">
<input
ref="startInputMins"
v-model="startInputMins"
type="number"
class="pr-time-input__mins"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('startInputMins')"
@wheel="increment($event, 'startInputMins')"
>
<b>:</b>
</div>
<div class="pr-time-input">
<input
ref="startInputSecs"
v-model="startInputSecs"
type="number"
class="pr-time-input__secs"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('startInputSecs')"
@wheel="increment($event, 'startInputSecs')"
>
</div>
<div class="pr-time-input pr-time-input__start-end-sep icon-arrows-right-left"></div>
<div class="pr-time-input">
<input
ref="endInputHrs"
v-model="endInputHrs"
class="pr-time-input__hrs"
step="1"
type="number"
min="0"
max="23"
title="Enter 0 - 23"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('endInputHrs')"
@wheel="increment($event, 'endInputHrs')"
>
<b>:</b>
</div>
<div class="pr-time-input">
<input
ref="endInputMins"
v-model="endInputMins"
type="number"
class="pr-time-input__mins"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('endInputMins')"
@wheel="increment($event, 'endInputMins')"
>
<b>:</b>
</div>
<div class="pr-time-input">
<input
ref="endInputSecs"
v-model="endInputSecs"
type="number"
class="pr-time-input__secs"
min="0"
max="59"
title="Enter 0 - 59"
step="1"
@change="validate()"
@keyup="validate()"
@focusin="selectAll($event)"
@focusout="format('endInputSecs')"
@wheel="increment($event, 'endInputSecs')"
>
</div>
<div class="pr-time-input pr-time-input--buttons">
<button
class="c-button c-button--major icon-check"
:disabled="isDisabled"
@click.prevent="submit"
></button>
<button
class="c-button icon-x"
@click.prevent="hide"
></button>
</div>
</form>
</template>
<script>
export default {
props: {
offsets: {
type: Object,
required: true
}
},
data() {
return {
startInputHrs: '00',
startInputMins: '00',
startInputSecs: '00',
endInputHrs: '00',
endInputMins: '00',
endInputSecs: '00',
isDisabled: false
};
},
watch: {
offsets: {
handler() {
this.setOffsets();
},
deep: true
}
},
mounted() {
this.setOffsets();
document.addEventListener('click', this.hide);
},
beforeDestroy() {
document.removeEventListener('click', this.hide);
},
methods: {
format(ref) {
const curVal = this[ref];
this[ref] = curVal.padStart(2, '0');
},
validate() {
let disabled = false;
let refs = ['startInputHrs', 'startInputMins', 'startInputSecs', 'endInputHrs', 'endInputMins', 'endInputSecs'];
for (let ref of refs) {
let min = Number(this.$refs[ref].min);
let max = Number(this.$refs[ref].max);
let value = Number(this.$refs[ref].value);
if (value > max || value < min) {
disabled = true;
break;
}
}
this.isDisabled = disabled;
},
submit() {
this.$emit('update', {
start: {
hours: this.startInputHrs,
minutes: this.startInputMins,
seconds: this.startInputSecs
},
end: {
hours: this.endInputHrs,
minutes: this.endInputMins,
seconds: this.endInputSecs
}
});
this.$emit('dismiss');
},
hide($event) {
if ($event.target.className.indexOf('c-button icon-x') > -1) {
this.$emit('dismiss');
}
},
increment($ev, ref) {
$ev.preventDefault();
const step = (ref === 'startInputHrs' || ref === 'endInputHrs') ? 1 : 5;
const maxVal = (ref === 'startInputHrs' || ref === 'endInputHrs') ? 23 : 59;
let cv = Math.round(parseInt(this[ref], 10) / step) * step;
cv = Math.min(maxVal, Math.max(0, ($ev.deltaY < 0) ? cv + step : cv - step));
this[ref] = cv.toString().padStart(2, '0');
this.validate();
},
setOffsets() {
[this.startInputHrs, this.startInputMins, this.startInputSecs] = this.offsets.start.split(':');
[this.endInputHrs, this.endInputMins, this.endInputSecs] = this.offsets.end.split(':');
this.numberSelect('startInputHrs');
},
numberSelect(input) {
this.$refs[input].focus();
// change to text, select, then change back to number
// number inputs do not support select()
this.$nextTick(() => {
if (this.$refs[input] === undefined) {
return;
}
this.$refs[input].setAttribute('type', 'text');
this.$refs[input].select();
this.$nextTick(() => {
this.$refs[input].setAttribute('type', 'number');
});
});
},
selectAll($ev) {
$ev.target.select();
}
}
};
</script>

View File

@@ -81,7 +81,6 @@ $colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: #ccc;
$colorAHov: #fff;
$filterHov: brightness(1.3) contrast(1.5); // Tree, location items
$filterHovSubtle: brightness(1.2) contrast(1.2);
$colorSelectedBg: rgba($colorKey, 0.3);
$colorSelectedFg: pullForward($colorBodyFg, 20%);
@@ -140,30 +139,13 @@ $colorBodyBgSubtleHov: pullForward($colorBodyBg, 10%);
$colorKeySubtle: pushBack($colorKey, 10%);
// Time Colors
$colorTimeFixed: #59554C;
$colorTimeFixedBg: $colorTimeFixed;
$colorTimeFixedFg: #eee;
$colorTimeFixedFgSubtle: #B2AA98;
$colorTimeFixedHov: pullForward($colorTimeFixed, 10%);
$colorTimeFixedSubtle: pushBack($colorTimeFixed, 20%);
$colorTimeFixedBtnBg: pullForward($colorTimeFixed, 5%);
$colorTimeFixedBtnFg: $colorTimeFixedFgSubtle;
$colorTimeFixedBtnBgMajor: #a09375;
$colorTimeFixedBtnFgMajor: #fff;
$colorTimeRealtime: #445890;
$colorTimeRealtimeBg: $colorTimeRealtime;
$colorTimeRealtimeFg: #eee;
$colorTimeRealtimeFgSubtle: #88B0FF;
$colorTimeRealtimeHov: pullForward($colorTimeRealtime, 10%);
$colorTimeRealtimeSubtle: pushBack($colorTimeRealtime, 20%);
$colorTimeRealtimeBtnBg: pullForward($colorTimeRealtime, 5%);
$colorTimeRealtimeBtnFg: $colorTimeRealtimeFgSubtle;
$colorTimeRealtimeBtnBgMajor: #588ffa;
$colorTimeRealtimeBtnFgMajor: #fff;
$colorTime: #618cff;
$colorTimeBg: $colorTime;
$colorTimeFg: pullForward($colorTimeBg, 30%);
$colorTimeHov: pullForward($colorTime, 10%);
$colorTimeSubtle: pushBack($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTimeRealtime; // was $timeControllerToiLineColorHov
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
$timeConductorAxisHoverFilter: brightness(1.2);
$timeConductorActiveBg: $colorKey;
$timeConductorActivePanBg: #226074;
@@ -259,7 +241,7 @@ $controlDisabledOpacity: 0.2;
$colorMenuBg: $colorBodyBg;
$colorMenuFg: $colorBodyFg;
$colorMenuIc: $colorKey;
$filterMenu: brightness(1.2);
$filterMenu: brightness(1.4);
$colorMenuHovBg: rgba($colorKey, 0.5);
$colorMenuHovFg: $colorBodyFgEm;
$colorMenuHovIc: $colorMenuHovFg;

View File

@@ -85,7 +85,6 @@ $colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: #ccc;
$colorAHov: #fff;
$filterHov: brightness(1.3) contrast(1.5); // Tree, location items
$filterHovSubtle: brightness(1.2) contrast(1.2);
$colorSelectedBg: rgba($colorKey, 0.3);
$colorSelectedFg: pullForward($colorBodyFg, 20%);
@@ -144,30 +143,13 @@ $colorBodyBgSubtleHov: pushBack($colorKey, 50%);
$colorKeySubtle: pushBack($colorKey, 10%);
// Time Colors
$colorTimeFixed: #59554C;
$colorTimeFixedBg: $colorTimeFixed;
$colorTimeFixedFg: #eee;
$colorTimeFixedFgSubtle: #B2AA98;
$colorTimeFixedHov: pullForward($colorTimeFixed, 10%);
$colorTimeFixedSubtle: pushBack($colorTimeFixed, 20%);
$colorTimeFixedBtnBg: pullForward($colorTimeFixed, 5%);
$colorTimeFixedBtnFg: $colorTimeFixedFgSubtle;
$colorTimeFixedBtnBgMajor: #a09375;
$colorTimeFixedBtnFgMajor: #fff;
$colorTimeRealtime: #445890;
$colorTimeRealtimeBg: $colorTimeRealtime;
$colorTimeRealtimeFg: #eee;
$colorTimeRealtimeFgSubtle: #88B0FF;
$colorTimeRealtimeHov: pullForward($colorTimeRealtime, 10%);
$colorTimeRealtimeSubtle: pushBack($colorTimeRealtime, 20%);
$colorTimeRealtimeBtnBg: pullForward($colorTimeRealtime, 5%);
$colorTimeRealtimeBtnFg: $colorTimeRealtimeFgSubtle;
$colorTimeRealtimeBtnBgMajor: #588ffa;
$colorTimeRealtimeBtnFgMajor: #fff;
$colorTime: #618cff;
$colorTimeBg: $colorTime;
$colorTimeFg: pullForward($colorTimeBg, 30%);
$colorTimeHov: pullForward($colorTime, 10%);
$colorTimeSubtle: pushBack($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTimeRealtime; // was $timeControllerToiLineColorHov
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
$timeConductorAxisHoverFilter: brightness(1.2);
$timeConductorActiveBg: $colorKey;
$timeConductorActivePanBg: #226074;

View File

@@ -81,7 +81,6 @@ $colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: $colorBodyFg;
$colorAHov: $colorKey;
$filterHov: hue-rotate(-10deg) brightness(0.8) contrast(2); // Tree, location items
$filterHovSubtle: hue-rotate(-8deg) brightness(0.5) contrast(1.2);
$colorSelectedBg: pushBack($colorKey, 40%);
$colorSelectedFg: pullForward($colorBodyFg, 10%);
@@ -140,30 +139,13 @@ $colorBodyBgSubtleHov: pullForward($colorBodyBg, 10%);
$colorKeySubtle: pushBack($colorKey, 20%);
// Time Colors
$colorTimeFixed: #59554C;
$colorTimeFixedBg: $colorTimeFixed;
$colorTimeFixedFg: #eee;
$colorTimeFixedFgSubtle: #B2AA98;
$colorTimeFixedHov: pullForward($colorTimeFixed, 10%);
$colorTimeFixedSubtle: pushBack($colorTimeFixed, 20%);
$colorTimeFixedBtnBg: pullForward($colorTimeFixed, 5%);
$colorTimeFixedBtnFg: $colorTimeFixedFgSubtle;
$colorTimeFixedBtnBgMajor: #a09375;
$colorTimeFixedBtnFgMajor: #fff;
$colorTimeRealtime: #445890;
$colorTimeRealtimeBg: $colorTimeRealtime;
$colorTimeRealtimeFg: #eee;
$colorTimeRealtimeFgSubtle: #88B0FF;
$colorTimeRealtimeHov: pullForward($colorTimeRealtime, 10%);
$colorTimeRealtimeSubtle: pushBack($colorTimeRealtime, 20%);
$colorTimeRealtimeBtnBg: pullForward($colorTimeRealtime, 5%);
$colorTimeRealtimeBtnFg: $colorTimeRealtimeFgSubtle;
$colorTimeRealtimeBtnBgMajor: #588ffa;
$colorTimeRealtimeBtnFgMajor: #fff;
$colorTime: #618cff;
$colorTimeBg: $colorTime;
$colorTimeFg: $colorBodyBg;
$colorTimeHov: pushBack($colorTime, 5%);
$colorTimeSubtle: pushBack($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTimeRealtime; // was $timeControllerToiLineColorHov
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
$timeConductorAxisHoverFilter: brightness(0.8);
$timeConductorActiveBg: $colorKey;
$timeConductorActivePanBg: #A0CDE1;

View File

@@ -244,13 +244,6 @@ button {
}
}
.c-not-button {
// Use within a holder that's clickable; use to indicate interactability
@include cButtonLayout();
cursor: pointer;
}
/******************************************************** DISCLOSURE CONTROLS */
/********* Disclosure Button */
// Provides a downward arrow icon that when clicked displays additional options and/or info.

View File

@@ -63,11 +63,6 @@ div {
}
}
.u-flex-spreader {
// Pushes against elements in a flex layout to spread them out
flex: 1 1 auto;
}
/******************************************************** BROWSER ELEMENTS */
body.desktop {
::-webkit-scrollbar {

View File

@@ -521,33 +521,24 @@
}
}
@mixin cButtonLayout() {
$pad: $interiorMargin;
padding: $pad floor($pad * 1.25);
@mixin cButton() {
@include cControl();
@include cControlHov();
@include themedButton();
border-radius: $controlCr;
color: $colorBtnFg;
cursor: pointer;
padding: $interiorMargin floor($interiorMargin * 1.25);
&:after,
> * + * {
margin-left: $interiorMarginSm;
}
&[class*='--compact'] {
padding: floor(math.div($pad, 1.5)) $pad;
}
}
@mixin cButton() {
@include cControl();
@include cControlHov();
@include themedButton();
@include cButtonLayout();
border-radius: $controlCr;
color: $colorBtnFg;
cursor: pointer;
&[class*="--major"],
&[class*='is-active']{
background: $colorBtnMajorBg !important;
color: $colorBtnMajorFg !important;
background: $colorBtnMajorBg;
color: $colorBtnMajorFg;
}
&[class*='--caution'] {
@@ -585,7 +576,7 @@
*:before {
// *:before handles any nested containers that may contain glyph elements
// Needed for c-togglebutton.
font-size: 1.15em;
font-size: 1.25em;
}
}

View File

@@ -61,17 +61,6 @@
'has-complex-content': complexContent
}"
>
<div
v-if="supportsIndependentTime"
class="c-conductor-holder--compact"
>
<independent-time-conductor
:domain-object="domainObject"
:object-path="[domainObject]"
@stateChanged="updateIndependentTimeState"
@updated="saveTimeOptions"
/>
</div>
<NotebookMenuSwitcher
v-if="notebookEnabled"
:domain-object="domainObject"
@@ -116,7 +105,6 @@
<script>
import ObjectView from './ObjectView.vue';
import NotebookMenuSwitcher from '@/plugins/notebook/components/NotebookMenuSwitcher.vue';
import IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue';
const SIMPLE_CONTENT_TYPES = [
'clock',
@@ -127,18 +115,10 @@ const SIMPLE_CONTENT_TYPES = [
];
const CSS_WIDTH_LESS_STR = '--width-less-than-';
const SupportedViewTypes = [
'plot-stacked',
'plot-overlay',
'bar-graph.view',
'time-strip.view'
];
export default {
components: {
ObjectView,
NotebookMenuSwitcher,
IndependentTimeConductor
NotebookMenuSwitcher
},
inject: ['openmct'],
props: {
@@ -183,11 +163,6 @@ export default {
computed: {
statusClass() {
return (this.status) ? `is-status--${this.status}` : '';
},
supportsIndependentTime() {
// const viewKey = this.getViewKey();
return true; //this.domainObject && SupportedViewTypes.includes(viewKey);
}
},
mounted() {
@@ -258,21 +233,6 @@ export default {
}
this.widthClass = wClass.trimStart();
},
getViewKey() {
let viewKey = this.this.$refs.objectView.viewKey;
if (this.objectViewKey) {
viewKey = this.objectViewKey;
}
return viewKey;
},
//Should the domainObject be updated in the Independent Time conductor component itself?
updateIndependentTimeState(useIndependentTime) {
this.openmct.objects.mutate(this.domainObject, 'configuration.useIndependentTime', useIndependentTime);
},
saveTimeOptions(options) {
this.openmct.objects.mutate(this.domainObject, 'configuration.timeOptions', options);
}
}
};

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div
v-if="supportsIndependentTime && false"
v-if="supportsIndependentTime"
class="c-conductor-holder--compact l-shell__main-independent-time-conductor"
>
<independent-time-conductor
@@ -74,7 +74,7 @@ export default {
return this.domainObject && (this.currentObjectPath || this.objectPath);
},
objectFontStyle() {
return this.domainObject && this.domainObject.configuration && this.domainObject.configuration.fontStyle;
return this.domainObject?.configuration?.fontStyle;
},
fontSize() {
return this.objectFontStyle ? this.objectFontStyle.fontSize : this.layoutFontSize;
@@ -287,6 +287,8 @@ export default {
this.$nextTick(() => {
this.updateStyle(this.styleRuleManager?.currentStyle);
this.setFontSize(this.fontSize);
this.setFont(this.font);
this.getActionCollection();
});
},
@@ -329,9 +331,9 @@ export default {
},
initObjectStyles() {
if (!this.styleRuleManager) {
this.styleRuleManager = new StyleRuleManager((this.domainObject.configuration && this.domainObject.configuration.objectStyles), this.openmct, this.updateStyle.bind(this), true);
this.styleRuleManager = new StyleRuleManager((this.domainObject.configuration?.objectStyles), this.openmct, this.updateStyle.bind(this), true);
} else {
this.styleRuleManager.updateObjectStyleConfig(this.domainObject.configuration && this.domainObject.configuration.objectStyles);
this.styleRuleManager.updateObjectStyleConfig(this.domainObject.configuration?.objectStyles);
}
if (this.stopListeningStyles) {
@@ -343,9 +345,6 @@ export default {
this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);
});
this.setFontSize(this.fontSize);
this.setFont(this.font);
this.stopListeningFontStyles = this.openmct.objects.observe(this.domainObject, 'configuration.fontStyle', (newFontStyle) => {
this.setFontSize(newFontStyle.fontSize);
this.setFont(newFontStyle.font);

View File

@@ -15,8 +15,6 @@
.c-object-label {
font-size: 1.05em;
min-width: 20%;
&__type-icon {
opacity: $objectLabelTypeIconOpacity;
}
@@ -39,8 +37,7 @@
/*************************** FRAME CONTROLS */
&__frame-controls {
display: flex;
flex: 0 1 auto;
overflow: hidden;
flex: 0 0 auto;
&__btns,
&__more {

View File

@@ -1,10 +1,11 @@
.c-object-label {
// <a> tag and draggable element that holds type icon and name.
// Used mostly in trees and lists
@include ellipsize();
display: flex;
align-items: center;
flex: 0 1 auto;
overflow: hidden;
white-space: nowrap;
> * + * { margin-left: $interiorMargin; }

View File

@@ -1,25 +1,9 @@
@use 'sass:math';
@mixin toggleSwitch($d: 12px, $m: 2px, $bg: $colorBtnBg) {
$br: math.div($d, 1.5);
.c-toggle-switch__slider {
background: $bg;
border-radius: $br;
height: $d + ($m*2);
width: $d*2 + $m*2;
&:before {
// Knob
border-radius: floor($br * 0.8);
box-shadow: rgba(black, 0.4) 0 0 2px;
height: $d; width: $d;
top: $m; left: $m; right: auto;
}
}
}
.c-toggle-switch {
$d: 12px;
$m: 2px;
$br: math.div($d, 1.5);
cursor: pointer;
display: inline-flex;
align-items: center;
@@ -36,26 +20,6 @@
display: block;
}
&__slider {
// Sits within __switch
display: inline-block;
position: relative;
&:before {
// Knob
background: $colorBtnFg; // TODO: make discrete theme constants for these colors
content: '';
display: block;
position: absolute;
transition: transform 100ms ease-in-out;
}
}
&__label {
margin-left: $interiorMarginSm;
white-space: nowrap;
}
input {
opacity: 0;
width: 0;
@@ -71,9 +35,31 @@
}
}
@include toggleSwitch();
}
&__slider {
// Sits within __switch
background: $colorBtnBg; // TODO: make discrete theme constants for these colors
border-radius: $br;
display: inline-block;
height: $d + ($m*2);
position: relative;
width: $d*2 + $m*2;
.c-toggle-switch--mini {
@include toggleSwitch($d: 9px, $m: 0px);
&:before {
// Knob
background: $colorBtnFg; // TODO: make discrete theme constants for these colors
border-radius: floor($br * 0.8);
box-shadow: rgba(black, 0.4) 0 0 2px;
content: '';
display: block;
position: absolute;
height: $d; width: $d;
top: $m; left: $m; right: auto;
transition: transform 100ms ease-in-out;
}
}
&__label {
margin-left: $interiorMarginSm;
white-space: nowrap;
}
}

View File

@@ -34,17 +34,6 @@
</div>
<div class="l-browse-bar__end">
<div
v-if="supportsIndependentTime"
class="c-conductor-holder--compact l-shell__main-independent-time-conductor"
>
<independent-time-conductor
:domain-object="domainObject"
:object-path="openmct.router.path"
@stateChanged="updateIndependentTimeState"
@updated="saveTimeOptions"
/>
</div>
<ViewSwitcher
v-if="!isEditing"
:current-view="currentView"
@@ -62,6 +51,7 @@
v-for="(item, index) in statusBarItems"
:key="index"
class="c-button"
:title="item.name"
:class="item.cssClass"
@click="item.onItemClicked"
>
@@ -137,20 +127,11 @@
<script>
import ViewSwitcher from './ViewSwitcher.vue';
import NotebookMenuSwitcher from '@/plugins/notebook/components/NotebookMenuSwitcher.vue';
import IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue';
const SupportedViewTypes = [
'plot-stacked',
'plot-overlay',
'bar-graph.view',
'time-strip.view'
];
const PLACEHOLDER_OBJECT = {};
export default {
components: {
IndependentTimeConductor,
NotebookMenuSwitcher,
ViewSwitcher
},
@@ -240,11 +221,6 @@ export default {
} else {
return 'Unlocked for editing - click to lock.';
}
},
supportsIndependentTime() {
const viewKey = this.getViewKey();
return this.domainObject && SupportedViewTypes.includes(viewKey);
}
},
watch: {
@@ -316,14 +292,6 @@ export default {
edit() {
this.openmct.editor.edit();
},
getViewKey() {
let viewKey = this.viewKey;
if (this.objectViewKey) {
viewKey = this.objectViewKey;
}
return viewKey;
},
promptUserandCancelEditing() {
let dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
@@ -400,13 +368,6 @@ export default {
},
setStatus(status) {
this.status = status;
},
//Should the domainObject be updated in the Independent Time conductor component itself?
updateIndependentTimeState(useIndependentTime) {
this.openmct.objects.mutate(this.domainObject, 'configuration.useIndependentTime', useIndependentTime);
},
saveTimeOptions(options) {
this.openmct.objects.mutate(this.domainObject, 'configuration.timeOptions', options);
}
}
};

View File

@@ -283,6 +283,17 @@
flex: 1 1 auto !important;
}
&__time-conductor {
border-top: 1px solid $colorInteriorBorder;
display: flex;
flex-direction: column;
padding-top: $interiorMargin;
> * + * {
margin-top: $interiorMargin;
}
}
&__main {
> .l-pane {
padding: nth($shellPanePad, 1) 0;
@@ -366,10 +377,10 @@
align-items: center;
justify-content: space-between;
//[class*="__"] {
// // Removes extraneous horizontal white space
// display: inline-flex;
//}
[class*="__"] {
// Removes extraneous horizontal white space
display: inline-flex;
}
> * + * {
margin-left: $interiorMarginSm;

View File

@@ -17,6 +17,7 @@
<button
v-if="isCollapsable"
class="l-pane__collapse-button c-icon-button"
:title="collapseTitle"
@click="toggleCollapse"
></button>
</div>
@@ -69,6 +70,9 @@ export default {
isCollapsable() {
return this.hideParam?.length > 0;
},
collapseTitle() {
return `Collapse ${this.label} Pane`;
},
localStorageKey() {
if (!this.label) {
return null;