Compare commits

...

26 Commits

Author SHA1 Message Date
a31e5e591a Use global time context 2022-06-30 19:54:48 -07:00
dfa0b54c7c Wait for clock initialization before requesting historical data 2022-06-30 14:48:27 -07:00
a040bb30c2 Gauge fixes for Firefox and units display (#5369)
* Closes #5323, #5325. Parent branch is release/2.0.5.
- Significant work refactoring SVG markup and CSS for dial gauge;
- Fixed missing `v-if` to control display of units for #5325;
- Fixed bad `.length` test for limit properties;

* Closes #5323, #5325
- Add 'value out of range' indicator

* Closes #5323, #5325
- More accurate element naming;
- Fix cross-browser problems with current value display in dial gauge;
- Refinements to "out of range" indicator approach;
- Fixed size of "Amplitude" input in Sine Wave Generator;

* Closes #5323, #5325
- Styles and stubbed in code to support needle meter type;

* Closes #5323, #5325
- Stubbed in markup and CSS for needle-style meter;

* Closes #5323, #5325
- Fixed missing `js-*` classes that were failing npm run test;

* Closes #5323, #5325
- Fix to not display meter value bar unless a data value is expected;

* Addressing PR comments
- Renamed method for clarity;
- Added null value check in method `valueExpected`;
2022-06-30 21:11:16 +02:00
0a2e0a4e65 [CouchDB] Better determination of indicator status (#5415)
* Add unknown state, remove maintenance state

* Handle all CouchDB status codes

- Set unknown status if we receive an unhandled code

* Include status code in error messages

* SharedWorker can send unknown status

* Add test for unknown status
2022-06-30 16:30:32 +00:00
e8df2bd437 Make plans non editable. (#5377)
* Make plans non editable.

* Add unit test for fix
2022-06-29 12:51:40 -07:00
ccd2a8b64c Plot progress bar fix for 2.0.5 (#5386)
* Add .bind(this) to stopLoading() in loadMoreData()

* Replace load spinner with progress bar for plots

* Add loading delay prop to swg

* fix linting errors

* match load order

* Update accessibility

* Add Math.max to timeout to handle negative inputs

* Moved math.max to load delay variable

* Add loading fix for stacked plots

* Move loadingUpdate func into plot item for update

* Merge conflict resolve

* Check if delay is 0 and send, put post in a func

* Put obj directly to model, removed computed prop

* Lint fix

* Fix template where legend was not displayed

* Remove commented out template

* Fixed failing test

Co-authored-by: unlikelyzero <jchill2@gmail.com>
2022-06-29 12:41:00 -07:00
2bd35bb2a5 5361 tags not persisting locally (#5408)
* fixed typo

* remove unneeded lookup

* fix tags adding and deleting

* more reliable way to remove tags

* break tests up for parallel execution

* fixed notebook tagging test

* enable e2e tests

* made schedule index comment more clear and fix uppercase/lowercase issue

* address e2e changes

* add unit test to bump coverage

* fix typo

* need to check on annotation creation if provider exists or not

* added fixtures

* undo silly couchdb commit
2022-06-29 19:30:18 +02:00
28dbd724d6 5391 Add preview and drag support to Grand Search (#5394)
* add preview and drag actions

* added unit test, simplified remove action

* do not hide search results in preview mode when clicking outside search results

* add semantic aria labels to enable e2e tests

* readd preview

* add e2e test

* remove commented out url

* add percy snapshot and add search to ci

* make percy stuff work

* linting

* fix percy again

* move percy snapshots to a visual test

* added separate visual test and changed test to fixtures

* fix fixtures path

* addressing review comments
2022-06-29 08:12:45 -07:00
5a1c329c66 [Timer] Update 3dot menu actions appropriately (#5387)
* Call `removeAllListeners()` after emit

* Manually show/hide actions if within a view

* remove sneaky `console.log()`

* Add Timer e2e test

* Add to comments

* Avoid hard waits in Timer e2e test

- Assert against timer view state instead of menu options

* Let's also test actions from the Timer view
2022-06-28 19:39:46 +02:00
00a5cbd2fd Cherrypicked commits (#5390)
Co-authored-by: unlikelyzero <jchill2@gmail.com>
2022-06-26 06:40:50 -07:00
a2d698d5c1 Imagery View does not discard old images when they fall out of bounds (#5351)
* change to using telemetry collection

* fix tests

* added more unit tests
2022-06-23 16:04:40 -07:00
5685a5b393 Fix naming of method (#5368) 2022-06-23 20:52:12 +00:00
164f39695e Remove workarounds for chrome 'scrollTop' issue (#5375) 2022-06-21 15:17:47 -07:00
c384cf67da Include objectStyles reference to conditionSetIdentifier in imports (#5354)
* Include objectStyles reference to conditionSetIdentifier in imports

* Add tests for export

* Refactored some code and removed console log
2022-06-21 14:34:45 -04:00
417b225505 Restrict timestrip composition to time based plots, plans and imagery (#5161)
* Restrict timestrip composition to time based plots, plans and imagery

* Adds unit tests for timeline composition policy

* Addresses review comments
Improves tests

* Reuse test objects

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2022-06-21 13:15:23 -04:00
e5e93f311c Port grid icons and imagery test to release 2.0.5 from master (#5360)
* Port grid icons to release 2.0.5 from master

* Port imagery test to release/2.0.5
2022-06-17 08:41:30 -07:00
39e6d9c90c Dont' mutate a stacked plot unless its user initiated (#5357) 2022-06-17 14:20:18 +00:00
60d021ef82 Fix imagery filter slider drag in flexible layouts (#5326) (#5350) 2022-06-16 12:25:29 -07:00
59880955a2 Remove snapshot 2022-06-08 19:11:40 -07:00
b51ed7e844 Merge branch 'master' of https://github.com/nasa/openmct 2022-06-08 19:11:13 -07:00
7bbaec4006 Merge branch 'master' of https://github.com/nasa/openmct 2022-06-07 14:02:58 -07:00
c0f24b3925 Merge branch 'master' of https://github.com/nasa/openmct 2022-05-31 11:06:55 -07:00
4e79725897 Merge branch 'master' of https://github.com/nasa/openmct 2022-05-24 15:05:16 -07:00
0674c9fc33 Merge branch 'master' of https://github.com/nasa/openmct 2022-05-20 09:25:39 -07:00
de1b877954 Merge branch 'master' of https://github.com/nasa/openmct 2022-05-09 14:00:43 -07:00
4db2f547d9 Bump d3-selection from 1.3.2 to 3.0.0
Bumps [d3-selection](https://github.com/d3/d3-selection) from 1.3.2 to 3.0.0.
- [Release notes](https://github.com/d3/d3-selection/releases)
- [Commits](https://github.com/d3/d3-selection/compare/v1.3.2...v3.0.0)

---
updated-dependencies:
- dependency-name: d3-selection
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-06 15:45:20 +00:00
81 changed files with 2398 additions and 705 deletions

View File

@ -0,0 +1,22 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1654548551471,\"end\":1654550351471}]}"
},
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654550352296,\"modified\":1654550352296},\"527856c0-cced-4b64-bb19-f943432326d0\":{\"identifier\":{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}}],\"yAxis\":{},\"xAxis\":{}},\"modified\":1654550353356,\"location\":\"mine\",\"persisted\":1654550353357},\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\"},\"modified\":1654550353350,\"location\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"persisted\":1654550353350}}"
},
{
"name": "mct-tree-expanded",
"value": "[\"/browse/mine\"]"
}
]
}
]
}

View File

@ -0,0 +1,22 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1654537164464,\"end\":1654538964464},{\"start\":1652301954635,\"end\":1652303754635}]}"
},
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654538965703,\"modified\":1654538965703},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
}
]
}
]
}

View File

@ -40,9 +40,6 @@ test.describe('Move item tests', () => {
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1);
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
@ -59,9 +56,6 @@ test.describe('Move item tests', () => {
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2);
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
@ -97,9 +91,6 @@ test.describe('Move item tests', () => {
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await page.locator('text=OK').click();
// Finish editing and save Telemetry Table

View File

@ -28,9 +28,7 @@ const { test } = require('../../fixtures.js');
const { expect } = require('@playwright/test');
const path = require('path');
// https://github.com/nasa/openmct/issues/4323#issuecomment-1067282651
test.describe('Persistence operations', () => {
test.describe('Persistence operations @addInit', () => {
// add non persistable root item
test.beforeEach(async ({ page }) => {
// eslint-disable-next-line no-undef
@ -38,6 +36,10 @@ test.describe('Persistence operations', () => {
});
test('Persistability should be respected in the create form location field', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/4323'
});
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });

View File

@ -52,7 +52,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
]);
//Save localStorage for future test execution
await context.storageState({ path: './e2e/tests/recycled_storage.json' });
await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' });
//Set object identifier from url
conditionSetUrl = await page.url();
@ -65,7 +65,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
await browser.close();
});
//Load localStorage for subsequent tests
test.use({ storageState: './e2e/tests/recycled_storage.json' });
test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
//Begin suite of tests again localStorage
test('Condition set object properties persist in main view and inspector', async ({ page }) => {
//Navigate to baseURL with injected localStorage

View File

@ -0,0 +1,30 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
// this will be called from the test suite with
// await page.addInitScript({ path: path.join(__dirname, 'addInitGauge.js') });
// it will install the Gauge since it is not installed by default
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.Gauge());
});

View File

@ -41,9 +41,6 @@ test.describe('Example Imagery', () => {
// Click text=Example Imagery
await page.click('text=Example Imagery');
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
@ -331,38 +328,12 @@ test('Example Imagery in Display layout', async ({ page }) => {
return newImageCount;
}, {
message: "verify that new images still stream in",
message: "verify that old images are discarded",
timeout: 6 * 1000
}).toBeGreaterThan(imageCount);
}).toBe(imageCount);
// Verify selected image is still displayed
await expect(selectedImage).toBeVisible();
// Unpause imagery
await page.locator('.pause-play').click();
//Get background-image url from background-image css prop
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
let backgroundImageUrl = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
});
let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
console.log('backgroundImageUrl1 ' + backgroundImageUrl1);
let backgroundImageUrl2;
await expect.poll(async () => {
// Verify next image has updated
let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
});
backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
return backgroundImageUrl2;
}, {
message: "verify next image has updated",
timeout: 6 * 1000
}).not.toBe(backgroundImageUrl1);
console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
});
test.describe('Example imagery thumbnails resize in display layouts', () => {

View File

@ -125,17 +125,17 @@ async function openContextMenuRestrictedNotebook(page) {
return;
}
test.describe('Restricted Notebook', () => {
test.describe('Restricted Notebook @addInit', () => {
test.beforeEach(async ({ page }) => {
await startAndAddNotebookObject(page);
});
test('Can be renamed', async ({ page }) => {
test('Can be renamed @addInit', async ({ page }) => {
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`);
});
test('Can be deleted if there are no locked pages', async ({ page }) => {
test('Can be deleted if there are no locked pages @addInit', async ({ page }) => {
await openContextMenuRestrictedNotebook(page);
const menuOptions = page.locator('.c-menu ul');
@ -158,7 +158,7 @@ test.describe('Restricted Notebook', () => {
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(0);
});
test('Can be locked if at least one page has one entry', async ({ page }) => {
test('Can be locked if at least one page has one entry @addInit', async ({ page }) => {
await enterTextEntry(page);
@ -168,7 +168,7 @@ test.describe('Restricted Notebook', () => {
});
test.describe('Restricted Notebook with at least one entry and with the page locked', () => {
test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => {
test.beforeEach(async ({ page }) => {
await startAndAddNotebookObject(page);
@ -179,7 +179,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
await page.locator('button.c-notebook__toggle-nav-button').click();
});
test('Locked page should now be in a locked state', async ({ page }) => {
test('Locked page should now be in a locked state @addInit', async ({ page }) => {
// main lock message on page
const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
expect.soft(await lockMessage.count()).toEqual(1);
@ -197,7 +197,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
});
test('Can still: add page, rename, add entry, delete unlocked pages', async ({ page }) => {
test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => {
// Click text=Page Add >> button
await Promise.all([
page.waitForNavigation(),
@ -237,14 +237,14 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
});
});
test.describe('Restricted Notebook with a page locked and with an embed', () => {
test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => {
test.beforeEach(async ({ page }) => {
await startAndAddNotebookObject(page);
await dragAndDropEmbed(page);
});
test('Allows embeds to be deleted if page unlocked', async ({ page }) => {
test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {
// Click .c-ne__embed__name .c-popup-menu-button
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
@ -252,7 +252,7 @@ test.describe('Restricted Notebook with a page locked and with an embed', () =>
await expect.soft(embedMenu).toContainText('Remove This Embed');
});
test('Disallows embeds to be deleted if page locked', async ({ page }) => {
test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => {
await lockPage(page);
// Click .c-ne__embed__name .c-popup-menu-button
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu

View File

@ -0,0 +1,146 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify form functionality.
*/
const { expect } = require('@playwright/test');
const { test } = require('../../../fixtures');
/**
* Creates a notebook object and adds an entry.
* @param {import('@playwright/test').Page} page
*/
async function createNotebookAndEntry(page) {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Notebook")
await page.locator('li:has-text("Notebook")').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
]);
// Click text=To start a new entry, click here or drag and drop any object
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
}
/**
* Creates a notebook object, adds an entry, and adds a tag.
* @param {import('@playwright/test').Page} page
*/
async function createNotebookEntryAndTags(page) {
await createNotebookAndEntry(page);
// Click text=To start a new entry, click here or drag and drop any object
await page.locator('button:has-text("Add Tag")').click();
// Click [placeholder="Type to select tag"]
await page.locator('[placeholder="Type to select tag"]').click();
// Click text=Driving
await page.locator('text=Driving').click();
// Click button:has-text("Add Tag")
await page.locator('button:has-text("Add Tag")').click();
// Click [placeholder="Type to select tag"]
await page.locator('[placeholder="Type to select tag"]').click();
// Click text=Science
await page.locator('text=Science').click();
}
test.describe('Tagging in Notebooks', () => {
test('Can load tags', async ({ page }) => {
await createNotebookAndEntry(page);
// Click text=To start a new entry, click here or drag and drop any object
await page.locator('button:has-text("Add Tag")').click();
// Click [placeholder="Type to select tag"]
await page.locator('[placeholder="Type to select tag"]').click();
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science");
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving");
});
test('Can add tags', async ({ page }) => {
await createNotebookEntryAndTags(page);
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
// Click button:has-text("Add Tag")
await page.locator('button:has-text("Add Tag")').click();
// Click [placeholder="Type to select tag"]
await page.locator('[placeholder="Type to select tag"]').click();
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science");
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving");
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
});
test('Can search for tags', async ({ page }) => {
await createNotebookEntryAndTags(page);
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
});
test('Can delete tags', async ({ page }) => {
await createNotebookEntryAndTags(page);
await page.locator('[aria-label="Notebook Entries"]').click();
// Delete Driving
await page.locator('text=Science Driving Add Tag >> button').nth(1).click();
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
});
test('Tags persist across reload', async ({ page }) => {
await createNotebookEntryAndTags(page);
//Go to baseURL
await page.reload();
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
});
});

View File

@ -39,9 +39,6 @@ test.describe('Telemetry Table', () => {
await page.locator(createButton).click();
await page.locator('li:has-text("Telemetry Table")').click();
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
@ -59,9 +56,6 @@ test.describe('Telemetry Table', () => {
// add Sine Wave Generator with defaults
await page.locator('li:has-text("Sine Wave Generator")').click();
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),

View File

@ -0,0 +1,184 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('Timer', () => {
test.beforeEach(async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Click 'Timer'
await page.click('text=Timer');
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK')
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
});
test('Can perform actions on the Timer', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/4313'
});
await test.step("From the tree context menu", async () => {
await triggerTimerContextMenuAction(page, 'Start');
await triggerTimerContextMenuAction(page, 'Pause');
await triggerTimerContextMenuAction(page, 'Restart at 0');
await triggerTimerContextMenuAction(page, 'Stop');
});
await test.step("From the 3dot menu", async () => {
await triggerTimer3dotMenuAction(page, 'Start');
await triggerTimer3dotMenuAction(page, 'Pause');
await triggerTimer3dotMenuAction(page, 'Restart at 0');
await triggerTimer3dotMenuAction(page, 'Stop');
});
await test.step("From the object view", async () => {
await triggerTimerViewAction(page, 'Start');
await triggerTimerViewAction(page, 'Pause');
await triggerTimerViewAction(page, 'Restart at 0');
});
});
});
/**
* Actions that can be performed on a timer from context menus.
* @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction
*/
/**
* Actions that can be performed on a timer from the object view.
* @typedef {'Start' | 'Pause' | 'Restart at 0'} TimerViewAction
*/
/**
* Open the timer context menu from the object tree.
* Expands the 'My Items' folder if it is not already expanded.
* @param {import('@playwright/test').Page} page
*/
async function openTimerContextMenu(page) {
const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3);
const className = await myItemsFolder.getAttribute('class');
if (!className.includes('c-disclosure-triangle--expanded')) {
await myItemsFolder.click();
}
await page.locator(`a:has-text("Unnamed Timer")`).click({
button: 'right'
});
}
/**
* Trigger a timer action from the tree context menu
* @param {import('@playwright/test').Page} page
* @param {TimerAction} action
*/
async function triggerTimerContextMenuAction(page, action) {
const menuAction = `.c-menu ul li >> text="${action}"`;
await openTimerContextMenu(page);
await page.locator(menuAction).click();
assertTimerStateAfterAction(page, action);
}
/**
* Trigger a timer action from the 3dot menu
* @param {import('@playwright/test').Page} page
* @param {TimerAction} action
*/
async function triggerTimer3dotMenuAction(page, action) {
const menuAction = `.c-menu ul li >> text="${action}"`;
const threeDotMenuButton = 'button[title="More options"]';
let isActionAvailable = false;
let iterations = 0;
// Dismiss/open the 3dot menu until the action is available
// or a maxiumum number of iterations is reached
while (!isActionAvailable && iterations <= 20) {
await page.click('.c-object-view');
await page.click(threeDotMenuButton);
isActionAvailable = await page.locator(menuAction).isVisible();
iterations++;
}
await page.locator(menuAction).click();
assertTimerStateAfterAction(page, action);
}
/**
* Trigger a timer action from the object view
* @param {import('@playwright/test').Page} page
* @param {TimerViewAction} action
*/
async function triggerTimerViewAction(page, action) {
const buttonTitle = buttonTitleFromAction(action);
await page.click(`button[title="${buttonTitle}"]`);
assertTimerStateAfterAction(page, action);
}
/**
* Takes in a TimerViewAction and returns the button title
* @param {TimerViewAction} action
*/
function buttonTitleFromAction(action) {
switch (action) {
case 'Start':
return 'Start';
case 'Pause':
return 'Pause';
case 'Restart at 0':
return 'Reset';
}
}
/**
* Verify the timer state after a timer action has been performed.
* @param {import('@playwright/test').Page} page
* @param {TimerAction} action
*/
async function assertTimerStateAfterAction(page, action) {
let timerStateClass;
switch (action) {
case 'Start':
case 'Restart at 0':
timerStateClass = "is-started";
break;
case 'Stop':
timerStateClass = 'is-stopped';
break;
case 'Pause':
timerStateClass = 'is-paused';
break;
}
await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass));
}

View File

@ -0,0 +1,111 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify search functionality.
*/
const { expect } = require('@playwright/test');
const { test } = require('../../../../fixtures');
/**
* Creates a notebook object and adds an entry.
* @param {import('@playwright/test').Page} page
*/
async function createClockAndDisplayLayout(page) {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Notebook")
await page.locator('li:has-text("Clock")').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
]);
// Click a:has-text("My Items")
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("My Items") >> nth=0').click()
]);
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Notebook")
await page.locator('li:has-text("Display Layout")').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
]);
}
test.describe('Grand Search', () => {
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => {
await createClockAndDisplayLayout(page);
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl');
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock');
// Click text=Elements >> nth=0
await page.locator('text=Elements').first().click();
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
// Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock
await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
await expect(page.locator('.js-preview-window')).toBeVisible();
// Click [aria-label="Close"]
await page.locator('[aria-label="Close"]').click();
await expect(page.locator('[aria-label="Search Result"]')).toBeVisible();
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Cloc');
// Click [aria-label="OpenMCT Search"] a >> nth=0
await page.locator('[aria-label="OpenMCT Search"] a').first().click();
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// Click text=Save and Finish Editing
await page.locator('text=Save and Finish Editing').click();
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
// Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
// Click text=Unnamed Clock
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Clock').click()
]);
await expect(page.locator('.is-object-type-clock')).toBeVisible();
});
});

View File

@ -0,0 +1,72 @@
/* eslint-disable no-undef */
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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.
*****************************************************************************/
/*
Collection of Visual Tests set to run with modified init scripts to inject plugins not otherwise available in the default contexts.
These should only use functional expect statements to verify assumptions about the state
in a test and not for functional verification of correctness. Visual tests are not supposed
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
*/
const { test } = require('@playwright/test');
const percySnapshot = require('@percy/playwright');
const path = require('path');
const sinon = require('sinon');
const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken
// Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758
// Will replace with cy.clock() equivalent
test.beforeEach(async ({ context }) => {
await context.addInitScript({
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
});
await context.addInitScript(() => {
window.__clock = sinon.useFakeTimers({
now: 0,
shouldAdvanceTime: true
}); //Set browser clock to UNIX Epoch
});
});
test('Visual - Default Gauge is correct @addInit', async ({ page }) => {
await page.addInitScript({ path: path.join(__dirname, '../plugins/gauge', './addInitGauge.js') });
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
await page.click('text=Gauge');
await page.click('text=OK');
// Take a snapshot of the newly created Gauge object
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Default Gauge');
});

View File

@ -0,0 +1,70 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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.
*****************************************************************************/
/*
Collection of Visual Tests set to run in a default context. The tests within this suite
are only meant to run against openmct's app.js started by `npm run start` within the
`./e2e/playwright-visual.config.js` file.
These should only use functional expect statements to verify assumptions about the state
in a test and not for functional verification of correctness. Visual tests are not supposed
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
*/
const { test, expect } = require('@playwright/test');
const percySnapshot = require('@percy/playwright');
const path = require('path');
const sinon = require('sinon');
// Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758
// Will replace with cy.clock() equivalent
test.beforeEach(async ({ context }) => {
await context.addInitScript({
// eslint-disable-next-line no-undef
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
});
await context.addInitScript(() => {
window.__clock = sinon.useFakeTimers({
now: 0, //Set browser clock to UNIX Epoch
shouldAdvanceTime: false, //Don't advance the clock
toFake: ["setTimeout", "nextTick"]
});
});
});
test.use({ storageState: './e2e/test-data/VisualTestData_storage.json' });
test('Visual - Overlay Plot Loading Indicator @localstorage', async ({ page }) => {
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
await page.locator('a:has-text("Unnamed Overlay Plot Overlay Plot")').click();
//Ensure that we're on the Unnamed Overlay Plot object
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
//Wait for canvas to be rendered and stop animating
await page.locator('canvas >> nth=1').hover({trial: true});
//Take snapshot of Sine Wave Generator within Overlay Plot
await percySnapshot(page, 'SineWaveInOverlayPlot');
});

View File

@ -0,0 +1,95 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to generating LocalStorage via Session Storage to be used
in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion
and generate an artifact named ./e2e/test-data/VisualTestData_storage.json . This will run
on every Commit to ensure that this object still loads into tests correctly and will retain the
.e2e.spec.js suffix.
TODO: Provide additional validation of object properties as it grows.
*/
const { test } = require('../../fixtures.js');
const { expect } = require('@playwright/test');
test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
await page.locator('button:has-text("Create")').click();
// add overlay plot with defaults
await page.locator('li:has-text("Overlay Plot")').click();
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
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 (exit edit mode)
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
await page.locator('text=Save and Finish Editing').click();
// click create button
await page.locator('button:has-text("Create")').click();
// add sine wave generator with defaults
await page.locator('li:has-text("Sine Wave Generator")').click();
//Add a 5000 ms Delay
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("Overlay Plot")');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
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'});
// focus the 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()
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
//Save localStorage for future test execution
await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' });
});

View File

@ -0,0 +1,104 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify search functionality.
*/
const { test, expect } = require('@playwright/test');
const percySnapshot = require('@percy/playwright');
/**
* Creates a notebook object and adds an entry.
* @param {import('@playwright/test').Page} page
*/
async function createClockAndDisplayLayout(page) {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Notebook")
await page.locator('li:has-text("Clock")').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
]);
// Click a:has-text("My Items")
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("My Items") >> nth=0').click()
]);
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Notebook")
await page.locator('li:has-text("Display Layout")').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
]);
}
test.describe('Grand Search', () => {
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => {
await createClockAndDisplayLayout(page);
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl');
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock');
await percySnapshot(page, 'Searching for Clocks');
// Click text=Elements >> nth=0
await page.locator('text=Elements').first().click();
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
// Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock
await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
await percySnapshot(page, 'Preview for clock should display when editing enabled and search item clicked');
// Click [aria-label="Close"]
await page.locator('[aria-label="Close"]').click();
await percySnapshot(page, 'Search should still be showing after preview closed');
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// Click text=Save and Finish Editing
await page.locator('text=Save and Finish Editing').click();
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
// Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
// Click text=Unnamed Clock
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Clock').click()
]);
await percySnapshot(page, 'Clicking on search results should navigate to them if not editing');
});
});

View File

@ -32,7 +32,8 @@ define([
offset: 0,
dataRateInHz: 1,
randomness: 0,
phase: 0
phase: 0,
loadDelay: 0
};
function GeneratorProvider(openmct) {
@ -53,8 +54,9 @@ define([
'period',
'offset',
'dataRateInHz',
'randomness',
'phase',
'randomness'
'loadDelay'
];
request = request || {};

View File

@ -116,6 +116,7 @@
var dataRateInHz = request.dataRateInHz;
var phase = request.phase;
var randomness = request.randomness;
var loadDelay = Math.max(request.loadDelay, 0);
var step = 1000 / dataRateInHz;
var nextStep = start - (start % step) + step;
@ -133,6 +134,14 @@
});
}
if (loadDelay === 0) {
postOnRequest(message, request, data);
} else {
setTimeout(() => postOnRequest(message, request, data), loadDelay);
}
}
function postOnRequest(message, request, data) {
self.postMessage({
id: message.id,
data: request.spectra ? {

View File

@ -81,7 +81,7 @@ define([
{
name: "Amplitude",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
cssClass: "l-numeric",
key: "amplitude",
required: true,
property: [
@ -92,7 +92,7 @@ define([
{
name: "Offset",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
cssClass: "l-numeric",
key: "offset",
required: true,
property: [
@ -132,6 +132,17 @@ define([
"telemetry",
"randomness"
]
},
{
name: "Loading Delay (ms)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "loadDelay",
required: true,
property: [
"telemetry",
"loadDelay"
]
}
],
initialize: function (object) {
@ -141,7 +152,8 @@ define([
offset: 0,
dataRateInHz: 1,
phase: 0,
randomness: 0
randomness: 0,
loadDelay: 0
};
}
});

View File

@ -168,7 +168,7 @@ function getImageUrlListFromConfig(configuration) {
}
function getImageLoadDelay(domainObject) {
const imageLoadDelay = domainObject.configuration.imageLoadDelayInMilliSeconds;
const imageLoadDelay = Math.trunc(Number(domainObject.configuration.imageLoadDelayInMilliSeconds));
if (!imageLoadDelay) {
openmctInstance.objects.mutate(domainObject, 'configuration.imageLoadDelayInMilliSeconds', DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS);
@ -190,7 +190,9 @@ function getRealtimeProvider() {
subscribe: (domainObject, callback) => {
const delay = getImageLoadDelay(domainObject);
const interval = setInterval(() => {
callback(pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay));
const imageSamples = getImageSamples(domainObject.configuration);
const datum = pointForTimestamp(Date.now(), domainObject.name, imageSamples, delay);
callback(datum);
}, delay);
return () => {
@ -229,8 +231,9 @@ function getLadProvider() {
},
request: (domainObject, options) => {
const delay = getImageLoadDelay(domainObject);
const datum = pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay);
return Promise.resolve([pointForTimestamp(Date.now(), domainObject.name, delay)]);
return Promise.resolve([datum]);
}
};
}

View File

@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "2.0.5-SNAPSHOT",
"version": "2.0.5",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.2",
@ -89,7 +89,7 @@
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance grandsearch notebook/tags",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots",
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js",

View File

@ -85,8 +85,6 @@ class ActionCollection extends EventEmitter {
}
destroy() {
super.removeAllListeners();
if (!this.skipEnvironmentObservers) {
this.objectUnsubscribes.forEach(unsubscribe => {
unsubscribe();
@ -96,6 +94,7 @@ class ActionCollection extends EventEmitter {
}
this.emit('destroy', this.view);
this.removeAllListeners();
}
getVisibleActions() {

View File

@ -60,6 +60,7 @@
tabindex="0"
:disabled="isInvalid"
class="c-button c-button--major"
aria-label="Save"
@click="onSave"
>
{{ submitLabel }}
@ -67,6 +68,7 @@
<button
tabindex="0"
class="c-button js-cancel-button"
aria-label="Cancel"
@click="onDismiss"
>
{{ cancelLabel }}

View File

@ -44,6 +44,7 @@
<div
v-if="!hideOptions"
class="c-menu c-input--autocomplete__options"
aria-label="Autocomplete Options"
@blur="hideOptions = true"
>
<ul>

View File

@ -28,6 +28,7 @@
>
<input
v-model="field"
:aria-label="model.name"
type="number"
:min="model.min"
:max="model.max"

View File

@ -224,7 +224,8 @@ class InMemorySearchProvider {
/**
* Schedule an id to be indexed at a later date. If there are less
* pending requests then allowed, will kick off an indexing request.
* pending requests than the maximum allowed, this will kick off an indexing request.
* This is done only when indexing first begins and we need to index a lot of objects.
*
* @private
* @param {identifier} id to be indexed.
@ -258,8 +259,12 @@ class InMemorySearchProvider {
}
onAnnotationCreation(annotationObject) {
const provider = this;
provider.index(annotationObject);
const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier);
if (objectProvider === undefined || objectProvider.search === undefined) {
const provider = this;
provider.index(annotationObject);
}
}
onNameMutation(domainObject, name) {
@ -270,7 +275,6 @@ class InMemorySearchProvider {
}
onTagMutation(domainObject, newTags) {
domainObject.oldTags = domainObject.tags;
domainObject.tags = newTags;
const provider = this;
@ -404,20 +408,16 @@ class InMemorySearchProvider {
}
});
// remove old tags
if (model.oldTags) {
model.oldTags.forEach(tagIDToRemove => {
const existsInNewModel = model.tags.includes(tagIDToRemove);
if (!existsInNewModel && this.localIndexedAnnotationsByTag[tagIDToRemove]) {
this.localIndexedAnnotationsByTag[tagIDToRemove] = this.localIndexedAnnotationsByTag[tagIDToRemove].
filter(annotationToRemove => {
const shouldKeep = annotationToRemove.keyString !== keyString;
const tagsToRemoveFromIndex = Object.keys(this.localIndexedAnnotationsByTag).filter(indexedTag => {
return !(model.tags.includes(indexedTag));
});
tagsToRemoveFromIndex.forEach(tagToRemoveFromIndex => {
this.localIndexedAnnotationsByTag[tagToRemoveFromIndex] = this.localIndexedAnnotationsByTag[tagToRemoveFromIndex].filter(indexedAnnotation => {
const shouldKeep = indexedAnnotation.keyString !== keyString;
return shouldKeep;
});
}
return shouldKeep;
});
}
});
}
localIndexAnnotation(objectToIndex, model) {
@ -449,7 +449,7 @@ class InMemorySearchProvider {
keyString
};
if (model && (model.type === 'annotation')) {
if (model.targets && model.targets) {
if (model.targets) {
this.localIndexAnnotation(objectToIndex, model);
}

View File

@ -94,19 +94,16 @@
});
// remove old tags
if (model.oldTags) {
model.oldTags.forEach(tagIDToRemove => {
const existsInNewModel = model.tags.includes(tagIDToRemove);
if (!existsInNewModel && indexedAnnotationsByTag[tagIDToRemove]) {
indexedAnnotationsByTag[tagIDToRemove] = indexedAnnotationsByTag[tagIDToRemove].
filter(annotationToRemove => {
const shouldKeep = annotationToRemove.keyString !== keyString;
const tagsToRemoveFromIndex = Object.keys(indexedAnnotationsByTag).filter(indexedTag => {
return !(model.tags.includes(indexedTag));
});
tagsToRemoveFromIndex.forEach(tagToRemoveFromIndex => {
indexedAnnotationsByTag[tagToRemoveFromIndex] = indexedAnnotationsByTag[tagToRemoveFromIndex].filter(indexedAnnotation => {
const shouldKeep = indexedAnnotation.keyString !== keyString;
return shouldKeep;
});
}
return shouldKeep;
});
}
});
}
function indexItem(keyString, model) {
@ -116,7 +113,7 @@
keyString
};
if (model && (model.type === 'annotation')) {
if (model.targets && model.targets) {
if (model.targets) {
indexAnnotation(objectToIndex, model);
}

View File

@ -315,7 +315,7 @@ define([
* @returns {Promise.<object[]>} a promise for an array of
* telemetry data
*/
TelemetryAPI.prototype.request = function (domainObject) {
TelemetryAPI.prototype.request = async function (domainObject) {
if (this.noRequestProviderForAllObjects) {
return Promise.resolve([]);
}
@ -337,6 +337,9 @@ define([
return this.handleMissingRequestProvider(domainObject);
}
const timeContext = this.openmct.time.getContextForView();
await timeContext.hasInitialTick();
return provider.request.apply(provider, arguments)
.catch((rejected) => {
if (rejected.name !== 'AbortError') {

View File

@ -344,6 +344,18 @@ class TimeContext extends EventEmitter {
return this.activeClock;
}
hasInitialTick() {
const waitForInitialTick = (resolve) => {
if (this.activeClock === undefined || this.activeClock.isInitialized()) {
resolve(true);
} else {
setTimeout(() => waitForInitialTick(resolve), 100);
}
};
return new Promise(waitForInitialTick);
}
/**
* Update bounds based on provided time and current offsets
* @param {number} timestamp A time from which bounds will be calculated

View File

@ -168,7 +168,7 @@ export default class StatusAPI extends EventEmitter {
*/
async resetStatusForRole(role) {
const provider = this.#userAPI.getProvider();
const defaultStatus = await this.getDefaultStatus();
const defaultStatus = await this.getDefaultStatusForRole(role);
if (provider.setStatusForRole) {
return provider.setStatusForRole(role, defaultStatus);

View File

@ -136,8 +136,8 @@ export default {
this.url = url;
}
const conditionSetIdentifier = domainObject.configuration.objectStyles.conditionSetIdentifier;
if (this.conditionSetIdentifier !== conditionSetIdentifier) {
const conditionSetIdentifier = domainObject.configuration?.objectStyles?.conditionSetIdentifier;
if (conditionSetIdentifier && this.conditionSetIdentifier !== conditionSetIdentifier) {
this.conditionSetIdentifier = conditionSetIdentifier;
}

View File

@ -128,6 +128,30 @@ export default class ExportAsJSONAction {
return copyOfChild;
}
/**
* @private
* @param {object} child
* @param {object} parent
* @returns {object}
*/
_rewriteLinkForReference(child, parent) {
const childId = this._getId(child);
this.externalIdentifiers.push(childId);
const copyOfChild = JSON.parse(JSON.stringify(child));
copyOfChild.identifier.key = uuid();
const newIdString = this._getId(copyOfChild);
const parentId = this._getId(parent);
this.idMap[childId] = newIdString;
copyOfChild.location = null;
parent.configuration.objectStyles.conditionSetIdentifier = copyOfChild.identifier;
this.tree[newIdString] = copyOfChild;
this.tree[parentId].configuration.objectStyles.conditionSetIdentifier = copyOfChild.identifier;
return copyOfChild;
}
/**
* @private
*/
@ -159,23 +183,27 @@ export default class ExportAsJSONAction {
"rootId": this._getId(this.root)
};
}
/**
* @private
* @param {object} parent
*/
_write(parent) {
this.calls++;
//conditional object styles are not saved on the composition, so we need to check for them
let childObjectReferenceId = parent.configuration?.objectStyles?.conditionSetIdentifier;
const composition = this.openmct.composition.get(parent);
if (composition !== undefined) {
composition.load()
.then((children) => {
children.forEach((child, index) => {
// Only export if object is creatable
// Only export if object is creatable
if (this._isCreatableAndPersistable(child)) {
// Prevents infinite export of self-contained objs
// Prevents infinite export of self-contained objs
if (!Object.prototype.hasOwnProperty.call(this.tree, this._getId(child))) {
// If object is a link to something absent from
// tree, generate new id and treat as new object
// If object is a link to something absent from
// tree, generate new id and treat as new object
if (this._isLinkedObject(child, parent)) {
child = this._rewriteLink(child, parent);
} else {
@ -186,18 +214,41 @@ export default class ExportAsJSONAction {
}
}
});
this.calls--;
if (this.calls === 0) {
this._rewriteReferences();
this._saveAs(this._wrapTree());
}
this._decrementCallsAndSave();
});
} else {
this.calls--;
if (this.calls === 0) {
this._rewriteReferences();
this._saveAs(this._wrapTree());
}
} else if (!childObjectReferenceId) {
this._decrementCallsAndSave();
}
if (childObjectReferenceId) {
this.openmct.objects.get(childObjectReferenceId)
.then((child) => {
// Only export if object is creatable
if (this._isCreatableAndPersistable(child)) {
// Prevents infinite export of self-contained objs
if (!Object.prototype.hasOwnProperty.call(this.tree, this._getId(child))) {
// If object is a link to something absent from
// tree, generate new id and treat as new object
if (this._isLinkedObject(child, parent)) {
child = this._rewriteLinkForReference(child, parent);
} else {
this.tree[this._getId(child)] = child;
}
this._write(child);
}
}
this._decrementCallsAndSave();
});
}
}
_decrementCallsAndSave() {
this.calls--;
if (this.calls === 0) {
this._rewriteReferences();
this._saveAs(this._wrapTree());
}
}
}

View File

@ -322,4 +322,57 @@ describe('Export as JSON plugin', () => {
exportAsJSONAction.invoke([parent]);
});
it('ExportAsJSONAction exports object references from tree', (done) => {
const parent = {
composition: [],
configuration: {
objectStyles: {
conditionSetIdentifier: {
key: 'child',
namespace: ''
}
}
},
identifier: {
key: 'parent',
namespace: ''
},
name: 'Parent',
type: 'folder',
modified: 1503598129176,
location: 'mine',
persisted: 1503598129176
};
const child = {
composition: [],
identifier: {
key: 'child',
namespace: ''
},
name: 'Child',
type: 'folder',
modified: 1503598132428,
location: null,
persisted: 1503598132428
};
spyOn(openmct.objects, 'get').and.callFake(object => {
return Promise.resolve(child);
});
spyOn(exportAsJSONAction, '_saveAs').and.callFake(completedTree => {
expect(Object.keys(completedTree).length).toBe(2);
const conditionSetId = Object.keys(completedTree.openmct)[1];
expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'parent')).toBeTruthy();
expect(completedTree.openmct[conditionSetId].name).toBe('Child');
done();
});
exportAsJSONAction.invoke([parent]);
});
});

View File

@ -472,7 +472,7 @@ describe('Gauge plugin', () => {
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
const valueElement = gaugeHolder.querySelector('.js-gauge-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
@ -485,7 +485,7 @@ describe('Gauge plugin', () => {
it('renders correct current value', (done) => {
function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.js-meter-current-value');
const textElement = gaugeHolder.querySelector('.js-gauge-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done();
}
@ -570,7 +570,7 @@ describe('Gauge plugin', () => {
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
const valueElement = gaugeHolder.querySelector('.js-gauge-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);

View File

@ -23,189 +23,217 @@
<div
class="c-gauge__wrapper js-gauge-wrapper"
:class="`c-gauge--${gaugeType}`"
:title="gaugeTitle"
>
<template v-if="typeDial">
<svg
width="0"
height="0"
class="c-dial__clip-paths"
class="c-gauge c-dial"
viewBox="0 0 10 10"
>
<defs>
<clipPath
id="gaugeBgMask"
clipPathUnits="objectBoundingBox"
>
<path d="M0.853553 0.853553C0.944036 0.763071 1 0.638071 1 0.5C1 0.223858 0.776142 0 0.5 0C0.223858 0 0 0.223858 0 0.5C0 0.638071 0.0559644 0.763071 0.146447 0.853553L0.285934 0.714066C0.23115 0.659281 0.197266 0.583598 0.197266 0.5C0.197266 0.332804 0.332804 0.197266 0.5 0.197266C0.667196 0.197266 0.802734 0.332804 0.802734 0.5C0.802734 0.583598 0.76885 0.659281 0.714066 0.714066L0.853553 0.853553Z" />
</clipPath>
<clipPath
id="gaugeValueMask"
clipPathUnits="objectBoundingBox"
>
<path d="M0.18926 0.81074C0.109735 0.731215 0.0605469 0.621351 0.0605469 0.5C0.0605469 0.257298 0.257298 0.0605469 0.5 0.0605469C0.742702 0.0605469 0.939453 0.257298 0.939453 0.5C0.939453 0.621351 0.890265 0.731215 0.81074 0.81074L0.714066 0.714066C0.76885 0.659281 0.802734 0.583599 0.802734 0.5C0.802734 0.332804 0.667196 0.197266 0.5 0.197266C0.332804 0.197266 0.197266 0.332804 0.197266 0.5C0.197266 0.583599 0.23115 0.659281 0.285934 0.714066L0.18926 0.81074Z" />
</clipPath>
</defs>
</svg>
<g class="c-dial__masks">
<mask id="gaugeValueMask">
<path
d="M1.8926 8.1074C1.09734 7.31215 0.605469 6.21352 0.605469 5C0.605469 2.57297 2.57297 0.605469 5 0.605469C7.42703 0.605469 9.39453 2.57297 9.39453 5C9.39453 6.21352 8.90266 7.31215 8.1074 8.1074L7.14066 7.14066C7.6885 6.59281 8.02734 5.83598 8.02734 5C8.02734 3.32804 6.67196 1.97266 5 1.97266C3.32804 1.97266 1.97266 3.32804 1.97266 5C1.97266 5.83598 2.3115 6.59281 2.85934 7.14066L1.8926 8.1074Z"
fill="white"
/>
</mask>
<mask id="gaugeBgMask">
<path
d="M8.53553 8.53553C9.44036 7.63071 10 6.38071 10 5C10 2.23858 7.76142 0 5 0C2.23858 0 0 2.23858 0 5C0 6.38071 0.559644 7.63071 1.46447 8.53553L2.85934 7.14066C2.3115 6.59281 1.97266 5.83598 1.97266 5C1.97266 3.32804 3.32804 1.97266 5 1.97266C6.67196 1.97266 8.02734 3.32804 8.02734 5C8.02734 5.83598 7.6885 6.59281 7.14066 7.14066L8.53553 8.53553Z"
fill="white"
/>
</mask>
</g>
<svg
class="c-dial__range c-gauge__range js-gauge-dial-range"
viewBox="0 0 512 512"
>
<text
v-if="displayMinMax"
font-size="35"
transform="translate(105 455) rotate(-45)"
>{{ rangeLow }}</text>
<text
v-if="displayMinMax"
font-size="35"
transform="translate(407 455) rotate(45)"
text-anchor="end"
>{{ rangeHigh }}</text>
</svg>
<svg
v-if="displayCurVal"
class="c-dial__current-value-text-wrapper"
viewBox="0 0 512 512"
>
<svg
class="c-dial__current-value-text-sizer"
:viewBox="curValViewBox"
<g
class="c-dial__graphics"
mask="url(#gaugeBgMask)"
>
<rect
class="c-dial__bg"
x="0"
y="0"
width="10"
height="10"
/>
<g
v-if="isDialLowLimit"
class="c-dial__limit-low"
:style="`transform: rotate(${dialLowLimitDeg}deg)`"
>
<rect
v-if="isDialLowLimitLow"
class="c-dial__low-limit__low"
x="5"
y="5"
width="5"
height="5"
/>
<rect
v-if="isDialLowLimitMid"
class="c-dial__low-limit__mid"
x="5"
y="0"
width="5"
height="5"
/>
<rect
v-if="isDialLowLimitHigh"
class="c-dial__low-limit__high"
x="0"
y="0"
width="5"
height="5"
/>
</g>
<g
v-if="isDialHighLimit"
class="c-dial__limit-high"
:style="`transform: rotate(${dialHighLimitDeg}deg)`"
>
<rect
v-if="isDialHighLimitLow"
class="c-dial__high-limit__low"
x="0"
y="5"
width="5"
height="5"
/>
<rect
v-if="isDialHighLimitMid"
class="c-dial__high-limit__mid"
x="0"
y="0"
width="5"
height="5"
/>
<rect
v-if="isDialHighLimitHigh"
class="c-dial__high-limit__high"
x="5"
y="0"
width="5"
height="5"
/>
</g>
</g>
<g
class="c-dial__graphics"
mask="url(#gaugeValueMask)"
>
<g
v-if="typeFilledDial"
class="c-dial__filled-value"
:style="`transform: rotate(${degValueFilledDial}deg)`"
>
<rect
v-if="isDialFilledValueLow"
class="c-dial__filled-value__low"
x="5"
y="5"
width="5"
height="5"
/>
<rect
v-if="isDialFilledValueMid"
class="c-dial__filled-value__mid"
x="5"
y="0"
width="5"
height="5"
/>
<rect
v-if="isDialFilledValueHigh"
class="c-dial__filled-value__high"
x="0"
y="0"
width="5"
height="5"
/>
</g>
<g
v-if="valueInBounds && typeNeedleDial"
class="c-dial__needle-value"
:style="`transform: rotate(${degValue}deg)`"
>
<path d="M4.90234 9.39453L5.09766 9.39453L5.30146 8.20874C6.93993 8.05674 8.22265 6.67817 8.22266 5C8.22266 3.22018 6.77982 1.77734 5 1.77734C3.22018 1.77734 1.77734 3.22018 1.77734 5C1.77734 6.67817 3.06007 8.05674 4.69854 8.20874L4.90234 9.39453Z" />
</g>
<path
id="dialTextPath"
class="c-dial__range-msg-path"
d="M8.3501 5.0001C8.3501 6.85025 6.85025 8.3501 5.0001 8.3501C3.14994 8.3501 1.6501 6.85025 1.6501 5.0001C1.6501 3.14994 3.14994 1.6501 5.0001 1.6501C6.85025 1.6501 8.3501 3.14994 8.3501 5.0001Z"
fill="none"
style="transform-origin: center; transform: rotate(182deg)"
/>
</g>
<g class="c-dial__text">
<text
x="50%"
y="70%"
text-anchor="middle"
class="c-gauge__units"
font-size="8%"
>{{ units }}</text>
<g
v-if="displayMinMax"
class="c-dial__range-text js-gauge-dial-range"
:font-size="rangeFontSize"
>
<text
transform="translate(1.5 8.7) rotate(-45)"
dominant-baseline="hanging"
>{{ rangeLow }}</text>
<text
transform="translate(8.4 8.7) rotate(45)"
dominant-baseline="hanging"
text-anchor="end"
>{{ rangeHigh }}</text>
</g>
</g>
<svg
v-if="!valueInBounds && valueExpected"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
xml:space="preserve"
class="c-dial__value-oor-indicator"
x="45%"
y="80%"
width="1"
height="1"
><path
d="M448 0H64C28.7.1.1 28.7 0 64v384c.1 35.3 28.7 63.9 64 64h384c35.3-.1 63.9-28.7 64-64V64c-.1-35.3-28.7-63.9-64-64zM288 448h-64v-64h64v64zm10.9-192L280 352h-48l-18.9-96V64H299v192h-.1z"
/></svg>
<svg
class="c-gauge__current-value-text-wrapper"
:viewBox="curValViewBox"
preserveAspectRatio="xMidYMid meet"
>
<rect
class="svg-viewbox-debug"
x="0"
y="0"
width="100%"
height="100%"
/>
<text
class="c-dial__current-value-text js-dial-current-value"
font-size="3.5"
lengthAdjust="spacing"
text-anchor="middle"
style="transform: translate(50%, 70%)"
>{{ curVal }}</text>
dominant-baseline="middle"
x="50%"
y="50%"
>
<template v-if="displayCurVal">
<tspan>{{ curVal }}</tspan>
</template>
</text>
</svg>
<svg
class="c-gauge__units c-dial__units"
viewBox="0 0 50 100"
>
<text
class="c-dial__units-text"
lengthAdjust="spacing"
text-anchor="middle"
style="transform: translate(50%, 72%)"
>{{ units }}</text>
</svg>
</svg>
<svg
class="c-dial__bg"
viewBox="0 0 10 10"
>
<g
v-if="isDialLowLimit"
class="c-dial__limit-low"
:style="`transform: rotate(${dialLowLimitDeg}deg)`"
>
<rect
v-if="isDialLowLimitLow"
class="c-dial__low-limit__low"
x="5"
y="5"
width="5"
height="5"
/>
<rect
v-if="isDialLowLimitMid"
class="c-dial__low-limit__mid"
x="5"
y="0"
width="5"
height="5"
/>
<rect
v-if="isDialLowLimitHigh"
class="c-dial__low-limit__high"
x="0"
y="0"
width="5"
height="5"
/>
</g>
<g
v-if="isDialHighLimit"
class="c-dial__limit-high"
:style="`transform: rotate(${dialHighLimitDeg}deg)`"
>
<rect
v-if="isDialHighLimitLow"
class="c-dial__high-limit__low"
x="0"
y="5"
width="5"
height="5"
/>
<rect
v-if="isDialHighLimitMid"
class="c-dial__high-limit__mid"
x="0"
y="0"
width="5"
height="5"
/>
<rect
v-if="isDialHighLimitHigh"
class="c-dial__high-limit__high"
x="5"
y="0"
width="5"
height="5"
/>
</g>
</svg>
<svg
v-if="typeFilledDial"
class="c-dial__filled-value-wrapper"
viewBox="0 0 10 10"
>
<g
class="c-dial__filled-value"
:style="`transform: rotate(${degValueFilledDial}deg)`"
>
<rect
v-if="isDialFilledValueLow"
class="c-dial__filled-value__low"
x="5"
y="5"
width="5"
height="5"
/>
<rect
v-if="isDialFilledValueMid"
class="c-dial__filled-value__mid"
x="5"
y="0"
width="5"
height="5"
/>
<rect
v-if="isDialFilledValueHigh"
class="c-dial__filled-value__high"
x="0"
y="0"
width="5"
height="5"
/>
</g>
</svg>
<svg
v-if="valueInBounds && typeNeedleDial"
class="c-dial__needle-value-wrapper"
viewBox="0 0 10 10"
>
<g
class="c-dial__needle-value"
:style="`transform: rotate(${degValue}deg)`"
>
<path d="M4.90234 9.39453L5.09766 9.39453L5.30146 8.20874C6.93993 8.05674 8.22265 6.67817 8.22266 5C8.22266 3.22018 6.77982 1.77734 5 1.77734C3.22018 1.77734 1.77734 3.22018 1.77734 5C1.77734 6.67817 3.06007 8.05674 4.69854 8.20874L4.90234 9.39453Z" />
</g>
</svg>
</template>
@ -219,9 +247,22 @@
<div class="c-meter__range__low">{{ rangeLow }}</div>
</div>
<div class="c-meter__bg">
<div
v-if="!valueInBounds && valueExpected"
class="c-meter__value-oor-indicator"
><svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
:preserveAspectRatio="meterOutOfRangeIndicatorAspectRatio"
><path
d="M448 0H64C28.7.1.1 28.7 0 64v384c.1 35.3 28.7 63.9 64 64h384c35.3-.1 63.9-28.7 64-64V64c-.1-35.3-28.7-63.9-64-64zM288 448h-64v-64h64v64zm10.9-192L280 352h-48l-18.9-96V64H299v192h-.1z"
/></svg></div>
<template v-if="typeMeterVertical">
<div
v-if="valueExpected"
class="c-meter__value"
:class="{'c-meter__value-needle' : typeNeedleMeter }"
:style="`transform: translateY(${meterValueToPerc}%)`"
></div>
@ -240,7 +281,9 @@
<template v-if="typeMeterHorizontal">
<div
v-if="valueExpected"
class="c-meter__value"
:class="{'c-meter__value-needle' : typeNeedleMeter }"
:style="`transform: translateX(${meterValueToPerc * -1}%)`"
></div>
@ -258,38 +301,42 @@
</template>
<svg
class="c-meter__current-value-text-wrapper"
viewBox="0 0 512 512"
class="c-gauge__current-value-text-wrapper"
:viewBox="curValViewBox"
preserveAspectRatio="xMidYMid meet"
>
<svg
v-if="displayCurVal"
class="c-meter__current-value-text-sizer"
:viewBox="curValViewBox"
preserveAspectRatio="xMidYMid meet"
<rect
class="svg-viewbox-debug"
x="0"
y="0"
width="100%"
height="100%"
/>
<text
class="c-meter__current-value-text js-gauge-current-value"
font-size="4"
lengthAdjust="spacing"
text-anchor="middle"
:dominant-baseline="meterTextBaseline"
x="50%"
y="50%"
>
<text
class="c-dial__current-value-text js-meter-current-value"
lengthAdjust="spacing"
text-anchor="middle"
style="transform: translate(50%, 70%)"
>
<template v-if="displayCurVal">
<tspan>{{ curVal }}</tspan>
<tspan
v-if="typeMeterHorizontal && displayUnits"
class="c-gauge__units"
font-size="10"
font-size="80%"
>{{ units }}</tspan>
</text>
<text
v-if="typeMeterVertical && displayUnits"
dy="12"
class="c-gauge__units"
font-size="10"
lengthAdjust="spacing"
text-anchor="middle"
style="transform: translate(50%, 70%)"
>{{ units }}</text>
</svg>
<tspan
v-if="typeMeterVertical && displayUnits"
x="50%"
dy="3.5"
class="c-gauge__units"
font-size="80%"
>{{ units }}</tspan>
</template>
</text>
</svg>
</div>
</div>
@ -343,14 +390,28 @@ export default {
dialLowLimitDeg() {
return this.percentToDegrees(this.valToPercent(this.limitLow));
},
meterOutOfRangeIndicatorAspectRatio() {
return this.typeMeterVertical ? 'xMidYMax meet' : 'xMinYMid meet';
},
meterTextBaseline() {
return this.typeMeterVertical ? 'auto' : 'middle';
},
curValViewBox() {
const DIGITS_RATIO = 10;
const VIEWBOX_STR = '0 0 X 15';
const DIGITS_RATIO = 3;
const VIEWBOX_STR = '0 0 X 10';
return VIEWBOX_STR.replace('X', this.digits * DIGITS_RATIO);
},
rangeFontSize() {
const CHAR_THRESHOLD = 3;
const START_PERC = 8.5;
const REDUCE_PERC = 0.8;
const RANGE_CHARS_MAX = Math.max(this.rangeLow.toString().length, this.rangeHigh.toString().length);
return this.fontSizeFromChars(RANGE_CHARS_MAX, CHAR_THRESHOLD, START_PERC, REDUCE_PERC);
},
isDialLowLimit() {
return this.limitLow.length > 0 && this.dialLowLimitDeg < getLimitDegree('low', 'max');
return this.limitLow.toString().length > 0 && this.dialLowLimitDeg < getLimitDegree('low', 'max');
},
isDialLowLimitLow() {
return this.dialLowLimitDeg >= getLimitDegree('low', 'q1');
@ -362,7 +423,7 @@ export default {
return this.dialLowLimitDeg >= getLimitDegree('low', 'q3');
},
isDialHighLimit() {
return this.limitHigh.length > 0 && this.dialHighLimitDeg < getLimitDegree('high', 'max');
return this.limitHigh.toString().length > 0 && this.dialHighLimitDeg < getLimitDegree('high', 'max');
},
isDialHighLimitLow() {
return this.dialHighLimitDeg <= getLimitDegree('high', 'max');
@ -383,10 +444,13 @@ export default {
return this.degValue >= getLimitDegree('low', 'q3');
},
isMeterLimitHigh() {
return this.limitHigh.length > 0 && this.meterHighLimitPerc > 0;
return this.limitHigh.toString().length > 0 && this.meterHighLimitPerc > 0;
},
isMeterLimitLow() {
return this.limitLow.length > 0 && this.meterLowLimitPerc > 0;
return this.limitLow.toString().length > 0 && this.meterLowLimitPerc > 0;
},
gaugeTitle() {
return this.valueInBounds ? 'Gauge' : 'Value is currently out of range and cannot be graphically displayed';
},
typeDial() {
return this.matchGaugeType('dial');
@ -409,15 +473,25 @@ export default {
typeMeterInverted() {
return this.matchGaugeType('inverted');
},
typeFilledMeter() {
return true; // Stubbing in for future capability
},
typeNeedleMeter() {
return false; // Stubbing in for future capability
},
meterValueToPerc() {
const meterDirection = (this.typeMeterInverted) ? -1 : 1;
if (this.curVal <= this.rangeLow) {
return meterDirection * 100;
}
if (this.typeFilledMeter) {
// Filled meter is a filled rectangle that is transformed along a vertical or horizontal axis
// So never move it below the low range more than 100%, or above the high range more than 0%
if (this.curVal <= this.rangeLow) {
return meterDirection * 100;
}
if (this.curVal >= this.rangeHigh) {
return 0;
if (this.curVal >= this.rangeHigh) {
return 0;
}
}
return this.valToPercentMeter(this.curVal) * meterDirection;
@ -428,6 +502,13 @@ export default {
meterLowLimitPerc() {
return 100 - this.valToPercentMeter(this.limitLow);
},
valueExpected() {
if (this.curVal === undefined || Object.is(this.curVal, 'null')) {
return false;
}
return this.curVal.toString().indexOf(DEFAULT_CURRENT_VALUE) === -1;
},
valueInBounds() {
return (this.curVal >= this.rangeLow && this.curVal <= this.rangeHigh);
},
@ -504,6 +585,11 @@ export default {
]
});
},
fontSizeFromChars(charNum, charThreshold, startPerc, reducePerc) {
const fs = (charNum <= charThreshold) ? startPerc : (startPerc - ((charNum - charThreshold) * reducePerc));
return fs.toString() + "%";
},
matchGaugeType(str) {
return this.gaugeType.indexOf(str) !== -1;
},

View File

@ -1,3 +1,8 @@
$meterNeedlePerc: 1%;
$meterNeedleMinPx: 4px;
$meterNeedleMaxPx: 20px;
$meterNeedleBorderRadius: 5px;
.is-object-type-gauge {
overflow: hidden;
}
@ -28,63 +33,64 @@
@include abs();
overflow: hidden;
}
&__current-value-text-wrapper {
// SVG
position: absolute;
height: 100%;
width: 100%;
}
}
.c-dial__value-oor-indicator,
.c-meter__value-oor-indicator {
fill: $colorGaugeRange;
opacity: 0.5;
}
/********************************************** DIAL GAUGE */
svg[class*='c-dial'] {
.c-dial {
max-height: 100%;
max-width: 100%;
position: absolute;
g {
transform-origin: center;
}
}
.c-dial {
&__bg {
background: $colorGaugeBg;
clip-path: url(#gaugeBgMask);
fill: $colorGaugeBg;
}
&__limit-high rect { fill: $colorGaugeLimitHigh; }
&__limit-low rect { fill: $colorGaugeLimitLow; }
&__filled-value-wrapper {
clip-path: url(#gaugeValueMask);
&__limit-high rect {
fill: $colorGaugeLimitHigh;
}
&__needle-value-wrapper {
clip-path: url(#gaugeValueMask);
&__limit-low rect {
fill: $colorGaugeLimitLow;
}
&__filled-value,
&__range-msg-text {
fill: $colorGaugeValue;
}
&__filled-value { fill: $colorGaugeValue; }
&__needle-value {
fill: $colorGaugeValue;
transition: transform $transitionTimeGauge;
}
&__current-value-text,
&__units-text {
&__current-value-text {
fill: $colorGaugeTextValue;
font-family: $heroFont;
}
&__units-text,
&__range-text {
fill: $colorGaugeRange;
}
&__graphics g {
transform-origin: center;
}
}
/********************************************** METER GAUGE */
.c-meter {
// Common styles for c-meter
$meterOutOfRangeIndicatorMaxSize: 50%;
@include abs();
display: flex;
svg {
// current-value-text
position: absolute;
height: 100%;
width: 100%;
}
&__range {
display: flex;
flex: 0 0 auto;
@ -102,10 +108,42 @@ svg[class*='c-dial'] {
// Filled area
position: absolute;
background: $colorGaugeValue;
transition: transform $transitionTimeGauge;
z-index: 1;
}
&__value-needle {
background: none !important;
&:before {
@include abs();
content: '';
display: block;
background: $colorGaugeValue;
}
}
&__value-oor-indicator {
$mxPx: 50px;
$wh: 50%;
position: absolute;
height: $wh;
width: $wh;
max-height: $mxPx;
max-width: $mxPx;
svg {
position: absolute;
width: 100%;
height: 100%;
max-height: 100%;
max-width: 100%;
}
}
&__current-value-text {
fill: $colorGaugeTextValue;
font-family: $heroFont;
}
.c-gauge__curval {
fill: $colorGaugeMeterTextValue !important;
}
@ -142,10 +180,28 @@ svg[class*='c-dial'] {
bottom: 0;
}
&__value-needle {
right: 0;
&:before {
border-bottom-left-radius: $meterNeedleBorderRadius;
border-top-left-radius: $meterNeedleBorderRadius;
height: $meterNeedlePerc;
min-height: $meterNeedleMinPx;
max-height: $meterNeedleMaxPx;
}
}
[class*='limit'] {
left: 0;
right: 0;
}
.c-meter__value-oor-indicator {
bottom: 10%;
left: 50%;
transform: translateX(-50%);
}
}
.c-gauge--meter-vertical & {
@ -156,6 +212,13 @@ svg[class*='c-dial'] {
&__limit-high {
top: 0;
}
&__value-needle {
&:before {
bottom: auto;
transform: translateY(-50%);
}
}
}
.c-gauge--meter-vertical-inverted & {
@ -174,6 +237,13 @@ svg[class*='c-dial'] {
&__range__high {
order: 2;
}
&__value-needle {
&:before {
top: auto;
transform: translateY(50%);
}
}
}
.c-gauge--meter-horizontal & {
@ -207,6 +277,20 @@ svg[class*='c-dial'] {
right: 0;
}
&__value-needle {
top: 0;
&:before {
border-bottom-left-radius: $meterNeedleBorderRadius;
border-bottom-right-radius: $meterNeedleBorderRadius;
left: auto;
width: $meterNeedlePerc;
min-width: $meterNeedleMinPx;
max-width: $meterNeedleMaxPx;
transform: translateX(50%);
}
}
[class*='limit'] {
top: 0;
bottom: 0;
@ -219,5 +303,16 @@ svg[class*='c-dial'] {
&__limit-high {
right: 0;
}
.c-meter__value-oor-indicator {
// Horizontal meter
left: 2%;
top: 50%;
transform: translateY(-50%);
}
}
}
.svg-viewbox-debug {
fill: rgba(deeppink, 0.5);
display: none;
}

View File

@ -14,6 +14,8 @@
type="range"
min="0"
max="500"
draggable="true"
@dragstart.stop.prevent
@change="notifyFiltersChanged"
@input="notifyFiltersChanged"
>
@ -24,6 +26,8 @@
type="range"
min="0"
max="500"
draggable="true"
@dragstart.stop.prevent
@change="notifyFiltersChanged"
@input="notifyFiltersChanged"
>

View File

@ -21,7 +21,11 @@
*****************************************************************************/
<template>
<div class="h-local-controls h-local-controls--overlay-content h-local-controls--menus-aligned c-local-controls--show-on-hover">
<div
class="h-local-controls h-local-controls--overlay-content h-local-controls--menus-aligned c-local-controls--show-on-hover"
role="toolbar"
aria-label="Image controls"
>
<imagery-view-menu-switcher
:icon-class="'icon-brightness'"
:title="'Brightness and contrast'"

View File

@ -30,7 +30,7 @@ export default {
this.timeSystemChange = this.timeSystemChange.bind(this);
this.setDataTimeContext = this.setDataTimeContext.bind(this);
this.setDataTimeContext();
this.openmct.objectViews.on('clearData', this.clearData);
this.openmct.objectViews.on('clearData', this.dataCleared);
// set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
@ -44,8 +44,11 @@ export default {
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
// kickoff
this.subscribe();
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {});
this.telemetryCollection.on('add', this.dataAdded);
this.telemetryCollection.on('remove', this.dataRemoved);
this.telemetryCollection.on('clear', this.dataCleared);
this.telemetryCollection.load();
},
beforeDestroy() {
if (this.unsubscribe) {
@ -54,9 +57,31 @@ export default {
}
this.stopFollowingDataTimeContext();
this.openmct.objectViews.off('clearData', this.clearData);
this.openmct.objectViews.off('clearData', this.dataCleared);
this.telemetryCollection.off('add', this.dataAdded);
this.telemetryCollection.off('remove', this.dataRemoved);
this.telemetryCollection.off('clear', this.dataCleared);
this.telemetryCollection.destroy();
},
methods: {
dataAdded(dataToAdd) {
const normalizedDataToAdd = dataToAdd.map(datum => this.normalizeDatum(datum));
this.imageHistory = this.imageHistory.concat(normalizedDataToAdd);
},
dataCleared() {
this.imageHistory = [];
},
dataRemoved(dataToRemove) {
this.imageHistory = this.imageHistory.filter(existingDatum => {
const shouldKeep = dataToRemove.some(datumToRemove => {
return (existingDatum.utc !== datumToRemove.utc);
});
return shouldKeep;
});
},
setDataTimeContext() {
this.stopFollowingDataTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
@ -70,19 +95,6 @@ export default {
this.timeContext.off('timeSystem', this.timeSystemChange);
}
},
isDatumValid(datum) {
//TODO: Add a check to see if there are duplicate images (identical image timestamp and url subsequently)
if (!datum) {
return false;
}
const datumTimeCheck = this.parseTime(datum);
const bounds = this.timeContext.bounds();
const isOutOfBounds = datumTimeCheck < bounds.start || datumTimeCheck > bounds.end;
return !isOutOfBounds;
},
formatImageUrl(datum) {
if (!datum) {
return;
@ -124,40 +136,6 @@ export default {
// forcibly reset the imageContainer size to prevent an aspect ratio distortion
delete this.imageContainerWidth;
delete this.imageContainerHeight;
return this.requestHistory();
},
async requestHistory() {
this.requestCount++;
const requestId = this.requestCount;
const bounds = this.timeContext.bounds();
const data = await this.openmct.telemetry
.request(this.domainObject, bounds) || [];
// wait until new request resolves to do comparison
if (this.requestCount !== requestId) {
return this.imageHistory = [];
}
const imagery = data.filter(this.isDatumValid).map(this.normalizeDatum);
this.imageHistory = imagery;
},
clearData(domainObjectToClear) {
// global clearData button is accepted therefore no truthy check on inputted param
const clearDataForObjectSelected = Boolean(domainObjectToClear);
if (clearDataForObjectSelected) {
const idsEqual = this.openmct.objects.areIdsEqual(
domainObjectToClear.identifier,
this.domainObject.identifier
);
if (!idsEqual) {
return;
}
}
// splice array to encourage garbage collection
this.imageHistory.splice(0, this.imageHistory.length);
},
timeSystemChange() {
this.timeSystem = this.timeContext.timeSystem();
@ -165,22 +143,7 @@ export default {
this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
},
subscribe() {
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.parseTime(datum);
let bounds = this.timeContext.bounds();
if (!(parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end)) {
return;
}
if (this.isDatumValid(datum)) {
this.imageHistory.push(this.normalizeDatum(datum));
}
});
},
normalizeDatum(datum) {
const formattedTime = this.formatTime(datum);
const url = this.formatImageUrl(datum);
const time = this.parseTime(formattedTime);

View File

@ -88,6 +88,7 @@ describe("The Imagery View Layouts", () => {
let openmct;
let parent;
let child;
let historicalProvider;
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
let imageryObject = {
identifier: {
@ -122,50 +123,6 @@ describe("The Imagery View Layouts", () => {
"priority": 3
},
"source": "url"
// "relatedTelemetry": {
// "heading": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "heading",
// "valueKey": "value"
// }
// },
// "roll": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "roll",
// "valueKey": "value"
// }
// },
// "pitch": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "pitch",
// "valueKey": "value"
// }
// },
// "cameraPan": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "cameraPan",
// "valueKey": "value"
// }
// },
// "cameraTilt": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "cameraTilt",
// "valueKey": "value"
// }
// },
// "sunOrientation": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "sunOrientation",
// "valueKey": "value"
// }
// }
// }
},
{
"name": "Name",
@ -209,6 +166,13 @@ describe("The Imagery View Layouts", () => {
telemetryPromiseResolve = resolve;
});
historicalProvider = {
request: () => {
return Promise.resolve(imageTelemetry);
}
};
spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider);
spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(imageTelemetry);
@ -584,6 +548,34 @@ describe("The Imagery View Layouts", () => {
expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);
done();
});
it('should reset the brightness and contrast when clicking the reset button', async () => {
const viewInstance = imageryView._getInstance();
await Vue.nextTick();
// Save the original brightness and contrast values
const origBrightness = viewInstance.$refs.ImageryContainer.filters.brightness;
const origContrast = viewInstance.$refs.ImageryContainer.filters.contrast;
// Change them to something else (default: 100)
viewInstance.$refs.ImageryContainer.setFilters({
brightness: 200,
contrast: 200
});
await Vue.nextTick();
// Verify that the values actually changed
expect(viewInstance.$refs.ImageryContainer.filters.brightness).toBe(200);
expect(viewInstance.$refs.ImageryContainer.filters.contrast).toBe(200);
// Click the reset button
parent.querySelector('.t-btn-reset').click();
await Vue.nextTick();
// Verify that the values were reset
expect(viewInstance.$refs.ImageryContainer.filters.brightness).toBe(origBrightness);
expect(viewInstance.$refs.ImageryContainer.filters.contrast).toBe(origContrast);
});
});
describe("imagery time strip view", () => {
@ -598,6 +590,20 @@ describe("The Imagery View Layouts", () => {
end: START + (5 * ONE_MINUTE)
});
const mockClock = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
mockClock.key = 'mockClock';
mockClock.currentValue.and.returnValue(1);
openmct.time.addClock(mockClock);
openmct.time.clock('mockClock', {
start: START - (5 * ONE_MINUTE),
end: START + (5 * ONE_MINUTE)
});
openmct.router.path = [{
identifier: {
key: 'test-timestrip',
@ -632,7 +638,7 @@ describe("The Imagery View Layouts", () => {
it("on mount should show imagery within the given bounds", (done) => {
Vue.nextTick(() => {
const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');
expect(imageElements.length).toEqual(6);
expect(imageElements.length).toEqual(5);
done();
});
});
@ -652,5 +658,46 @@ describe("The Imagery View Layouts", () => {
});
});
});
it("should remove images when clock advances", async () => {
openmct.time.tick(ONE_MINUTE * 2);
await Vue.nextTick();
await Vue.nextTick();
const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');
expect(imageElements.length).toEqual(4);
});
it("should remove images when start bounds shorten", async () => {
openmct.time.timeSystem('utc', {
start: START,
end: START + (5 * ONE_MINUTE)
});
await Vue.nextTick();
await Vue.nextTick();
const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');
expect(imageElements.length).toEqual(1);
});
it("should remove images when end bounds shorten", async () => {
openmct.time.timeSystem('utc', {
start: START - (5 * ONE_MINUTE),
end: START - (2 * ONE_MINUTE)
});
await Vue.nextTick();
await Vue.nextTick();
const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');
expect(imageElements.length).toEqual(4);
});
it("should remove images when both bounds shorten", async () => {
openmct.time.timeSystem('utc', {
start: START - (2 * ONE_MINUTE),
end: START + (2 * ONE_MINUTE)
});
await Vue.nextTick();
await Vue.nextTick();
const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');
expect(imageElements.length).toEqual(3);
});
});
});

View File

@ -82,12 +82,14 @@ export default class ImportAsJSONAction {
* @param {object} seen
*/
_deepInstantiate(parent, tree, seen) {
if (this.openmct.composition.get(parent)) {
let objectIdentifiers = this._getObjectReferenceIds(parent);
if (objectIdentifiers.length) {
let newObj;
seen.push(parent.id);
parent.composition.forEach(async (childId) => {
objectIdentifiers.forEach(async (childId) => {
const keystring = this.openmct.objects.makeKeyString(childId);
if (!tree[keystring] || seen.includes(keystring)) {
return;
@ -101,6 +103,27 @@ export default class ImportAsJSONAction {
}, this);
}
}
/**
* @private
* @param {object} parent
* @returns [identifiers]
*/
_getObjectReferenceIds(parent) {
let objectIdentifiers = [];
let parentComposition = this.openmct.composition.get(parent);
if (parentComposition) {
objectIdentifiers = Array.from(parentComposition.domainObject.composition);
}
//conditional object styles are not saved on the composition, so we need to check for them
let parentObjectReference = parent.configuration?.objectStyles?.conditionSetIdentifier;
if (parentObjectReference) {
objectIdentifiers.push(parentObjectReference);
}
return objectIdentifiers;
}
/**
* @private
* @param {object} tree

View File

@ -144,6 +144,7 @@
v-if="selectedSection && selectedPage"
ref="notebookEntries"
class="c-notebook__entries"
aria-label="Notebook Entries"
>
<NotebookEntry
v-for="entry in filteredAndSortedEntries"

View File

@ -23,6 +23,7 @@
<template>
<div
class="c-notebook__entry c-ne has-local-controls has-tag-applier"
aria-label="Notebook Entry"
:class="{ 'locked': isLocked }"
@dragover="changeCursor"
@drop.capture="cancelEditMode"

View File

@ -93,8 +93,7 @@
message.state = 'close';
break;
default:
// Assume connection is closed
message.state = 'close';
message.state = 'unknown';
console.error('🚨 Received unexpected readyState value from CouchDB EventSource feed: 🚨', readyState);
break;
}

View File

@ -22,7 +22,7 @@
import CouchDocument from "./CouchDocument";
import CouchObjectQueue from "./CouchObjectQueue";
import { PENDING, CONNECTED, DISCONNECTED } from "./CouchStatusIndicator";
import { PENDING, CONNECTED, DISCONNECTED, UNKNOWN } from "./CouchStatusIndicator";
import { isNotebookType } from '../../notebook/notebook-constants.js';
const REV = "_rev";
@ -112,7 +112,7 @@ class CouchObjectProvider {
* Takes in a state message from the CouchDB SharedWorker and returns an IndicatorState.
* @private
* @param {'open'|'close'|'pending'} message
* @returns import('./CouchStatusIndicator').IndicatorState
* @returns {import('./CouchStatusIndicator').IndicatorState}
*/
#messageToIndicatorState(message) {
let state;
@ -126,14 +126,52 @@ class CouchObjectProvider {
case 'pending':
state = PENDING;
break;
default:
state = PENDING;
case 'unknown':
state = UNKNOWN;
break;
}
return state;
}
/**
* Takes an HTTP status code and returns an IndicatorState
* @private
* @param {number} statusCode
* @returns {import("./CouchStatusIndicator").IndicatorState}
*/
#statusCodeToIndicatorState(statusCode) {
let state;
switch (statusCode) {
case CouchObjectProvider.HTTP_OK:
case CouchObjectProvider.HTTP_CREATED:
case CouchObjectProvider.HTTP_ACCEPTED:
case CouchObjectProvider.HTTP_NOT_MODIFIED:
case CouchObjectProvider.HTTP_BAD_REQUEST:
case CouchObjectProvider.HTTP_UNAUTHORIZED:
case CouchObjectProvider.HTTP_FORBIDDEN:
case CouchObjectProvider.HTTP_NOT_FOUND:
case CouchObjectProvider.HTTP_METHOD_NOT_ALLOWED:
case CouchObjectProvider.HTTP_NOT_ACCEPTABLE:
case CouchObjectProvider.HTTP_CONFLICT:
case CouchObjectProvider.HTTP_PRECONDITION_FAILED:
case CouchObjectProvider.HTTP_REQUEST_ENTITY_TOO_LARGE:
case CouchObjectProvider.HTTP_UNSUPPORTED_MEDIA_TYPE:
case CouchObjectProvider.HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
case CouchObjectProvider.HTTP_EXPECTATION_FAILED:
case CouchObjectProvider.HTTP_SERVER_ERROR:
state = CONNECTED;
break;
case CouchObjectProvider.HTTP_SERVICE_UNAVAILABLE:
state = DISCONNECTED;
break;
default:
state = UNKNOWN;
}
return state;
}
//backwards compatibility, options used to be a url. Now it's an object
#normalize(options) {
if (typeof options === 'string') {
@ -163,21 +201,11 @@ class CouchObjectProvider {
let response = null;
try {
response = await fetch(this.url + '/' + subPath, fetchOptions);
this.indicator.setIndicatorToState(CONNECTED);
const { status } = response;
const json = await response.json();
this.#handleResponseCode(status, json, fetchOptions);
if (response.status === CouchObjectProvider.HTTP_CONFLICT) {
throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
} else if (response.status === CouchObjectProvider.HTTP_BAD_REQUEST
|| response.status === CouchObjectProvider.HTTP_UNAUTHORIZED
|| response.status === CouchObjectProvider.HTTP_NOT_FOUND
|| response.status === CouchObjectProvider.HTTP_PRECONDITION_FAILED) {
const error = await response.json();
throw new Error(`CouchDB Error: "${error.error}: ${error.reason}"`);
} else if (response.status === CouchObjectProvider.HTTP_SERVER_ERROR) {
throw new Error('CouchDB Error: "500 Internal Server Error"');
}
return await response.json();
return json;
} catch (error) {
// Network error, CouchDB unreachable.
if (response === null) {
@ -188,6 +216,24 @@ class CouchObjectProvider {
}
}
/**
* Handle the response code from a CouchDB request.
* Sets the CouchDB indicator status and throws an error if needed.
* @private
*/
#handleResponseCode(status, json, fetchOptions) {
this.indicator.setIndicatorToState(this.#statusCodeToIndicatorState(status));
if (status === CouchObjectProvider.HTTP_CONFLICT) {
throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
} else if (status >= CouchObjectProvider.HTTP_BAD_REQUEST) {
if (!json.error || !json.reason) {
throw new Error(`CouchDB Error ${status}`);
}
throw new Error(`CouchDB Error ${status}: "${json.error} - ${json.reason}"`);
}
}
/**
* Check the response to a create/update/delete request;
* track the rev if it's valid, otherwise return false to
@ -627,11 +673,25 @@ class CouchObjectProvider {
}
}
// https://docs.couchdb.org/en/3.2.0/api/basics.html
CouchObjectProvider.HTTP_OK = 200;
CouchObjectProvider.HTTP_CREATED = 201;
CouchObjectProvider.HTTP_ACCEPTED = 202;
CouchObjectProvider.HTTP_NOT_MODIFIED = 304;
CouchObjectProvider.HTTP_BAD_REQUEST = 400;
CouchObjectProvider.HTTP_UNAUTHORIZED = 401;
CouchObjectProvider.HTTP_FORBIDDEN = 403;
CouchObjectProvider.HTTP_NOT_FOUND = 404;
CouchObjectProvider.HTTP_METHOD_NOT_ALLOWED = 404;
CouchObjectProvider.HTTP_NOT_ACCEPTABLE = 406;
CouchObjectProvider.HTTP_CONFLICT = 409;
CouchObjectProvider.HTTP_PRECONDITION_FAILED = 412;
CouchObjectProvider.HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
CouchObjectProvider.HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
CouchObjectProvider.HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
CouchObjectProvider.HTTP_EXPECTATION_FAILED = 417;
CouchObjectProvider.HTTP_SERVER_ERROR = 500;
// If CouchDB is containerized via Docker it will return 503 if service is unavailable.
CouchObjectProvider.HTTP_SERVICE_UNAVAILABLE = 503;
export default CouchObjectProvider;

View File

@ -102,7 +102,7 @@ class CouchSearchProvider {
},
{
"model.annotationType": {
"$eq": "notebook"
"$eq": "NOTEBOOK"
}
},
{

View File

@ -56,10 +56,10 @@ export const DISCONNECTED = {
description: "CouchDB is offline and unavailable for requests."
};
/** @type {IndicatorState} */
export const MAINTENANCE = {
statusClass: "s-status-warning-lo",
text: "CouchDB is in maintenance mode",
description: "CouchDB is online, but not currently accepting requests."
export const UNKNOWN = {
statusClass: "s-status-info",
text: "CouchDB connectivity unknown",
description: "CouchDB is in an unknown state of connectivity."
};
export default class CouchStatusIndicator {

View File

@ -25,7 +25,7 @@ import {
createOpenMct,
resetApplicationState, spyOnBuiltins
} from 'utils/testing';
import { CONNECTED, DISCONNECTED, PENDING } from './CouchStatusIndicator';
import { CONNECTED, DISCONNECTED, PENDING, UNKNOWN } from './CouchStatusIndicator';
describe('the plugin', () => {
let openmct;
@ -397,5 +397,38 @@ describe('the view', () => {
assertCouchIndicatorStatus(PENDING);
});
it("to 'unknown'", async () => {
const workerMessage = {
data: {
type: 'state',
state: 'unknown'
}
};
mockPromise = Promise.resolve({
status: 200,
json: () => {
return {
ok: true,
_id: 'some-value',
id: 'some-value',
_rev: 1,
model: {}
};
}
});
fetch.and.returnValue(mockPromise);
await openmct.objects.get({
namespace: '',
key: 'object-1'
});
// Simulate 'pending' state from worker message
provider.onSharedWorkerMessage(workerMessage);
await Vue.nextTick();
assertCouchIndicatorStatus(UNKNOWN);
});
});
});

View File

@ -39,7 +39,7 @@ export default function PlanViewProvider(openmct) {
},
canEdit(domainObject) {
return domainObject.type === 'plan';
return false;
},
view: function (domainObject, objectPath) {

View File

@ -96,6 +96,18 @@ describe('the plugin', function () {
let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
expect(planView).toBeDefined();
});
it('is not an editable view', () => {
const testViewObject = {
id: "test-object",
type: "plan"
};
openmct.router.path = [testViewObject];
const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);
let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
expect(planView.canEdit()).toBeFalse();
});
});
describe('the plan view displays activities', () => {

View File

@ -484,7 +484,7 @@ export default {
end: range.max,
domain: this.config.xAxis.get('key')
})
.then(this.stopLoading());
.then(this.stopLoading.bind(this));
if (purge) {
plotSeries.purgeRecordsOutsideRange(range);
}

View File

@ -24,16 +24,16 @@
ref="plotWrapper"
class="c-plot holder holder-plot has-control-bar"
>
<div
ref="plotContainer"
class="l-view-section u-style-receiver js-style-receiver"
:class="{'s-status-timeconductor-unsynced': status && status === 'timeconductor-unsynced'}"
>
<div
<progress-bar
v-show="!!loading"
class="c-loading--overlay loading"
></div>
class="c-telemetry-table__progress-bar"
:model="{progressPerc: undefined}"
/>
<mct-plot
:init-grid-lines="gridLines"
:init-cursor-guide="cursorGuide"
@ -49,10 +49,12 @@
import eventHelpers from './lib/eventHelpers';
import ImageExporter from '../../exporters/ImageExporter';
import MctPlot from './MctPlot.vue';
import ProgressBar from "../../ui/components/ProgressBar.vue";
export default {
components: {
MctPlot
MctPlot,
ProgressBar
},
inject: ['openmct', 'domainObject', 'path'],
props: {
@ -89,17 +91,14 @@ export default {
destroy() {
this.stopListening();
},
exportJPG() {
const plotElement = this.$refs.plotContainer;
this.imageExporter.exportJPG(plotElement, 'plot.jpg', 'export-plot');
},
exportPNG() {
const plotElement = this.$refs.plotContainer;
this.imageExporter.exportPNG(plotElement, 'plot.png', 'export-plot');
},
setStatus(status) {
this.status = status;
},

View File

@ -64,6 +64,7 @@ import YAxisForm from "./forms/YAxisForm.vue";
import LegendForm from "./forms/LegendForm.vue";
import eventHelpers from "../lib/eventHelpers";
import configStore from "../configuration/ConfigStore";
import _ from "lodash";
export default {
components: {
@ -125,15 +126,25 @@ export default {
});
if (index < 0) {
index = stackedPlotObject.configuration.series.length;
const configPath = `configuration.series[${index}]`;
let newConfig = {
identifier: config.identifier
};
_.set(newConfig, `${config.path}`, config.value);
this.openmct.objects.mutate(
stackedPlotObject,
configPath,
newConfig
);
} else {
const configPath = `configuration.series[${index}].${config.path}`;
this.openmct.objects.mutate(
stackedPlotObject,
configPath,
config.value
);
}
const configPath = `configuration.series[${index}].${config.path}`;
this.openmct.objects.mutate(
stackedPlotObject,
configPath,
config.value
);
}
}
};

View File

@ -227,8 +227,9 @@ export default {
const path = objectPath(formField.objectPath);
if (!_.isEqual(newVal, oldVal)) {
// TODO: Why do we mutate yAxis twice, once directly, once via objects.mutate? Or are they different objects?
// We mutate the model for the plots first PlotConfigurationModel - this triggers changes that affects the plot behavior
this.yAxis.set(formKey, newVal);
// Then we mutate the domain object configuration to persist the settings
if (path) {
if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
this.$emit('seriesUpdated', {

View File

@ -167,18 +167,6 @@ export default {
const id = this.openmct.objects.makeKeyString(child.identifier);
this.$set(this.tickWidthMap, id, 0);
const persistedConfig = this.domainObject.configuration.series && this.domainObject.configuration.series.find((seriesConfig) => {
return this.openmct.objects.areIdsEqual(seriesConfig.identifier, child.identifier);
});
if (persistedConfig === undefined) {
this.openmct.objects.mutate(
this.domainObject,
'configuration.series[' + this.compositionObjects.length + ']',
{
identifier: child.identifier
}
);
}
this.compositionObjects.push(child);
},

View File

@ -29,6 +29,7 @@ import Vue from "vue";
import conditionalStylesMixin from "./mixins/objectStyles-mixin";
import configStore from "@/plugins/plot/configuration/ConfigStore";
import PlotConfigurationModel from "@/plugins/plot/configuration/PlotConfigurationModel";
import ProgressBar from "../../../ui/components/ProgressBar.vue";
export default {
mixins: [conditionalStylesMixin],
@ -125,7 +126,6 @@ export default {
const onConfigLoaded = this.onConfigLoaded;
const onCursorGuideChange = this.onCursorGuideChange;
const onGridLinesChange = this.onGridLinesChange;
const loadingUpdated = this.loadingUpdated;
const setStatus = this.setStatus;
const openmct = this.openmct;
@ -140,7 +140,8 @@ export default {
this.component = new Vue({
el: viewContainer,
components: {
MctPlot
MctPlot,
ProgressBar
},
provide: {
openmct,
@ -156,11 +157,16 @@ export default {
onConfigLoaded,
onCursorGuideChange,
onGridLinesChange,
loadingUpdated,
setStatus
setStatus,
loading: true
};
},
template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\'}"><div v-show="!!loading" class="c-loading--overlay loading"></div><mct-plot :init-grid-lines="gridLines" :init-cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :limit-line-labels="limitLineLabels" :color-palette="colorPalette" :options="options" @plotTickWidth="onTickWidthChange" @lockHighlightPoint="onLockHighlightPointUpdated" @highlights="onHighlightsUpdated" @configLoaded="onConfigLoaded" @cursorGuide="onCursorGuideChange" @gridLines="onGridLinesChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
methods: {
loadingUpdated(loaded) {
this.loading = loaded;
}
},
template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\'}"><progress-bar v-show="loading !== false" class="c-telemetry-table__progress-bar" :model="{progressPerc: undefined}" /><mct-plot :init-grid-lines="gridLines" :init-cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :limit-line-labels="limitLineLabels" :color-palette="colorPalette" :options="options" @plotTickWidth="onTickWidthChange" @lockHighlightPoint="onLockHighlightPointUpdated" @highlights="onHighlightsUpdated" @configLoaded="onConfigLoaded" @cursorGuide="onCursorGuideChange" @gridLines="onGridLinesChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
});
this.setSelection();
@ -198,17 +204,12 @@ export default {
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context);
},
loadingUpdated(loaded) {
this.loading = loaded;
this.updateComponentProp('loading', loaded);
},
getProps() {
return {
limitLineLabels: this.showLimitLineLabels,
gridLines: this.gridLines,
cursorGuide: this.cursorGuide,
plotTickWidth: this.plotTickWidth,
loading: this.loading,
options: this.options,
status: this.status,
colorPalette: this.colorPalette
@ -223,33 +224,39 @@ export default {
const configId = this.openmct.objects.makeKeyString(this.childObject.identifier);
let config = configStore.get(configId);
if (!config) {
const persistedConfig = this.domainObject.configuration.series.find((seriesConfig) => {
let persistedSeriesConfig = this.domainObject.configuration.series.find((seriesConfig) => {
return this.openmct.objects.areIdsEqual(seriesConfig.identifier, this.childObject.identifier);
});
if (persistedConfig) {
config = new PlotConfigurationModel({
id: configId,
domainObject: {
...this.childObject,
configuration: {
series: [
{
identifier: this.childObject.identifier,
...persistedConfig.series
}
],
yAxis: persistedConfig.yAxis
}
},
openmct: this.openmct,
palette: this.colorPalette,
callback: (data) => {
this.data = data;
}
});
configStore.add(configId, config);
if (!persistedSeriesConfig) {
persistedSeriesConfig = {
series: {},
yAxis: {}
};
}
config = new PlotConfigurationModel({
id: configId,
domainObject: {
...this.childObject,
configuration: {
series: [
{
identifier: this.childObject.identifier,
...persistedSeriesConfig.series
}
],
yAxis: persistedSeriesConfig.yAxis
}
},
openmct: this.openmct,
palette: this.colorPalette,
callback: (data) => {
this.data = data;
}
});
configStore.add(configId, config);
}
return this.childObject;

View File

@ -459,7 +459,7 @@ describe("the plugin", function () {
max: 10
});
Vue.nextTick(() => {
expect(plotViewComponentObject.$children[1].component.$children[0].xScale.domain()).toEqual({
expect(plotViewComponentObject.$children[1].component.$children[1].xScale.domain()).toEqual({
min: 0,
max: 10
});
@ -473,7 +473,7 @@ describe("the plugin", function () {
max: 20
});
Vue.nextTick(() => {
expect(plotViewComponentObject.$children[1].component.$children[0].yScale.domain()).toEqual({
expect(plotViewComponentObject.$children[1].component.$children[1].yScale.domain()).toEqual({
min: 10,
max: 20
});

View File

@ -112,6 +112,7 @@ export default class RemoteClock extends DefaultClock {
if (time > this.lastTick) {
this.tick(time);
this.initialized = true;
}
}

View File

@ -123,10 +123,7 @@ export default class RemoveAction {
}
if (isEditing) {
let currentItemInView = this.openmct.router.path[0];
let domainObject = objectPath[0];
if (this.openmct.objects.areIdsEqual(currentItemInView.identifier, domainObject.identifier)) {
if (this.openmct.router.isNavigatedObject(objectPath)) {
return false;
}
}

View File

@ -144,7 +144,7 @@
<progress-bar
v-if="loading"
class="c-telemetry-table__progress-bar"
:model="progressLoad"
:model="{progressPerc: undefined}"
/>
<!-- Headers table -->
@ -385,11 +385,6 @@ export default {
};
},
computed: {
progressLoad() {
return {
progressPerc: undefined
};
},
dropTargetStyle() {
return {
top: this.$refs.headersTable.offsetTop + 'px',

View File

@ -0,0 +1,70 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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 ALLOWED_TYPES = [
'telemetry.plot.overlay',
'telemetry.plot.stacked',
'plan'
];
const DISALLOWED_TYPES = [
'telemetry.plot.bar-graph',
'telemetry.plot.scatter-plot'
];
export default function TimelineCompositionPolicy(openmct) {
function hasNumericTelemetry(domainObject, metadata) {
const hasTelemetry = openmct.telemetry.isTelemetryObject(domainObject);
if (!hasTelemetry || !metadata) {
return false;
}
return metadata.values().length > 0 && hasDomainAndRange(metadata);
}
function hasDomainAndRange(metadata) {
return (metadata.valuesForHints(['range']).length > 0
&& metadata.valuesForHints(['domain']).length > 0);
}
function hasImageTelemetry(domainObject, metadata) {
if (!metadata) {
return false;
}
return metadata.valuesForHints(['image']).length > 0;
}
return {
allow: function (parent, child) {
if (parent.type === 'time-strip') {
const metadata = openmct.telemetry.getMetadata(child);
if (!DISALLOWED_TYPES.includes(child.type)
&& (hasNumericTelemetry(child, metadata) || hasImageTelemetry(child, metadata) || ALLOWED_TYPES.includes(child.type))) {
return true;
}
return false;
}
return true;
}
};
}

View File

@ -22,6 +22,7 @@
import TimelineViewProvider from './TimelineViewProvider';
import timelineInterceptor from "./timelineInterceptor";
import TimelineCompositionPolicy from "./TimelineCompositionPolicy";
export default function () {
return function install(openmct) {
@ -39,6 +40,8 @@ export default function () {
}
});
timelineInterceptor(openmct);
openmct.composition.addPolicy(new TimelineCompositionPolicy(openmct).allow);
openmct.objectViews.addProvider(new TimelineViewProvider(openmct));
};
}

View File

@ -62,6 +62,34 @@ describe('the plugin', function () {
})
}
};
let timelineObject = {
"composition": [],
configuration: {
useIndependentTime: false,
timeOptions: {
mode: {
key: 'fixed'
},
fixedOffsets: {
start: 10,
end: 11
},
clockOffsets: {
start: -(30 * 60 * 1000),
end: (30 * 60 * 1000)
}
}
},
"name": "Some timestrip",
"type": "time-strip",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
beforeEach((done) => {
mockObjectPath = [
@ -134,28 +162,7 @@ describe('the plugin', function () {
beforeEach(() => {
testViewObject = {
id: "test-object",
identifier: {
key: "test-object",
namespace: ''
},
type: "time-strip",
configuration: {
useIndependentTime: false,
timeOptions: {
mode: {
key: 'fixed'
},
fixedOffsets: {
start: 10,
end: 11
},
clockOffsets: {
start: -(30 * 60 * 1000),
end: (30 * 60 * 1000)
}
}
}
...timelineObject
};
const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath);
@ -187,15 +194,7 @@ describe('the plugin', function () {
beforeEach(() => {
timelineDomainObject = {
identifier: {
key: 'test-object',
namespace: ''
},
type: 'time-strip',
id: "test-object",
configuration: {
useIndependentTime: false
},
...timelineObject,
composition: [
{
identifier: {
@ -236,27 +235,10 @@ describe('the plugin', function () {
describe('the independent time conductor', () => {
let timelineView;
let testViewObject = {
id: "test-object",
identifier: {
key: "test-object",
namespace: ''
},
type: "time-strip",
...timelineObject,
configuration: {
useIndependentTime: true,
timeOptions: {
mode: {
key: 'local'
},
fixedOffsets: {
start: 10,
end: 11
},
clockOffsets: {
start: -(30 * 60 * 1000),
end: (30 * 60 * 1000)
}
}
...timelineObject.configuration,
useIndependentTime: true
}
};
@ -284,27 +266,15 @@ describe('the plugin', function () {
describe('the independent time conductor - fixed', () => {
let timelineView;
let testViewObject2 = {
...timelineObject,
id: "test-object2",
identifier: {
key: "test-object2",
namespace: ''
},
type: "time-strip",
configuration: {
useIndependentTime: true,
timeOptions: {
mode: {
key: 'fixed'
},
fixedOffsets: {
start: 10,
end: 11
},
clockOffsets: {
start: -(30 * 60 * 1000),
end: (30 * 60 * 1000)
}
}
...timelineObject.configuration,
useIndependentTime: true
}
};
@ -328,4 +298,68 @@ describe('the plugin', function () {
});
});
describe("The timestrip composition policy", () => {
let testObject;
beforeEach(() => {
testObject = {
...timelineObject,
composition: []
};
});
it("allows composition for plots", () => {
const testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "some-key",
name: "Some attribute",
hints: {
domain: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 1
}
}]
}
};
const composition = openmct.composition.get(testObject);
expect(() => {
composition.add(testTelemetryObject);
}).not.toThrow();
expect(testObject.composition.length).toBe(1);
});
it("allows composition for plans", () => {
const composition = openmct.composition.get(testObject);
expect(() => {
composition.add(planObject);
}).not.toThrow();
expect(testObject.composition.length).toBe(1);
});
it("disallows composition for non time-based plots", () => {
const barGraphObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "telemetry.plot.bar-graph",
name: "Test Object"
};
const composition = openmct.composition.get(testObject);
expect(() => {
composition.add(barGraphObject);
}).toThrow();
expect(testObject.composition.length).toBe(0);
});
});
});

View File

@ -43,14 +43,19 @@ export default class PauseTimerAction {
this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);
}
appliesTo(objectPath) {
appliesTo(objectPath, view = {}) {
const domainObject = objectPath[0];
if (!domainObject || !domainObject.configuration) {
return;
}
// Use object configuration timerState for viewless context menus,
// otherwise manually show/hide based on the view's timerState
const viewKey = view.key;
const { timerState } = domainObject.configuration;
return domainObject.type === 'timer' && timerState === 'started';
return viewKey
? domainObject.type === 'timer'
: domainObject.type === 'timer' && timerState === 'started';
}
}

View File

@ -44,14 +44,19 @@ export default class RestartTimerAction {
this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);
}
appliesTo(objectPath) {
appliesTo(objectPath, view = {}) {
const domainObject = objectPath[0];
if (!domainObject || !domainObject.configuration) {
return;
}
// Use object configuration timerState for viewless context menus,
// otherwise manually show/hide based on the view's timerState
const viewKey = view.key;
const { timerState } = domainObject.configuration;
return domainObject.type === 'timer' && timerState !== 'stopped';
return viewKey
? domainObject.type === 'timer'
: domainObject.type === 'timer' && timerState !== 'stopped';
}
}

View File

@ -63,14 +63,19 @@ export default class StartTimerAction {
newConfiguration.pausedTime = undefined;
this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);
}
appliesTo(objectPath) {
appliesTo(objectPath, view = {}) {
const domainObject = objectPath[0];
if (!domainObject || !domainObject.configuration) {
return;
}
// Use object configuration timerState for viewless context menus,
// otherwise manually show/hide based on the view's timerState
const viewKey = view.key;
const { timerState } = domainObject.configuration;
return domainObject.type === 'timer' && timerState !== 'started';
return viewKey
? domainObject.type === 'timer'
: domainObject.type === 'timer' && timerState !== 'started';
}
}

View File

@ -44,14 +44,19 @@ export default class StopTimerAction {
this.openmct.objects.mutate(domainObject, 'configuration', newConfiguration);
}
appliesTo(objectPath) {
appliesTo(objectPath, view = {}) {
const domainObject = objectPath[0];
if (!domainObject || !domainObject.configuration) {
return;
}
// Use object configuration timerState for viewless context menus,
// otherwise manually show/hide based on the view's timerState
const viewKey = view.key;
const { timerState } = domainObject.configuration;
return domainObject.type === 'timer' && timerState !== 'stopped';
return viewKey
? domainObject.type === 'timer'
: domainObject.type === 'timer' && timerState !== 'stopped';
}
}

View File

@ -179,6 +179,15 @@ export default {
return timerSign;
}
},
watch: {
timerState() {
if (!this.viewActionsCollection) {
return;
}
this.showOrHideAvailableActions();
}
},
mounted() {
this.$nextTick(() => {
if (this.configuration && this.configuration.timerState === undefined) {
@ -190,6 +199,9 @@ export default {
this.unlisten = ticker.listen(() => {
this.openmct.objects.refresh(this.domainObject);
});
this.viewActionsCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
this.showOrHideAvailableActions();
});
},
beforeDestroy() {
@ -228,6 +240,22 @@ export default {
if (action) {
action.invoke(this.objectPath, this.currentView);
}
},
showOrHideAvailableActions() {
switch (this.timerState) {
case 'started':
this.viewActionsCollection.hide(['timer.start']);
this.viewActionsCollection.show(['timer.stop', 'timer.pause', 'timer.restart']);
break;
case 'paused':
this.viewActionsCollection.hide(['timer.pause']);
this.viewActionsCollection.show(['timer.stop', 'timer.start', 'timer.restart']);
break;
case 'stopped':
this.viewActionsCollection.hide(['timer.stop', 'timer.pause', 'timer.restart']);
this.viewActionsCollection.show(['timer.start']);
break;
}
}
}
};

View File

@ -40,6 +40,7 @@ export default class LocalClock extends DefaultClock {
this.period = period;
this.timeoutHandle = undefined;
this.lastTick = Date.now();
this.initialized = true;
}
start() {

View File

@ -283,34 +283,32 @@ $bg-icon-info: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3
$bg-icon-plus: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M480,192H320V32A32.1,32.1,0,0,0,288,0H224a32.1,32.1,0,0,0-32,32V192H32A32.1,32.1,0,0,0,0,224v64a32.1,32.1,0,0,0,32,32H192V480a32.1,32.1,0,0,0,32,32h64a32.1,32.1,0,0,0,32-32V320H480a32.1,32.1,0,0,0,32-32V224A32.1,32.1,0,0,0,480,192Z' transform='translate(0)'/%3e%3c/svg%3e");
$bg-icon-grippy-ew: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M416 0v512h-64V0zM288 0v512h-64V0zM160 0v512H96V0z'/%3e%3c/svg%3e");
$bg-icon-chain-links: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M479.2 32.8C457.3 10.9 428.7 0 400 0c-28.7 0-57.3 10.9-79.2 32.8l-64 64c-37 37-42.7 93.5-17 136.5l-6.4 6.4C215.7 229.3 195.9 224 176 224c-28.7 0-57.3 10.9-79.2 32.8l-64 64c-43.7 43.7-43.7 114.7 0 158.4C54.7 501.1 83.3 512 112 512c28.7 0 57.3-10.9 79.2-32.8l64-64c37-37 42.7-93.5 17-136.5l6.4-6.4c17.6 10.5 37.5 15.8 57.3 15.8 28.7 0 57.3-10.9 79.2-32.8l64-64c43.8-43.8 43.8-114.8.1-158.5zM209.9 369.9l-64 64c-9 9.1-21.1 14.1-33.9 14.1-12.8 0-24.9-5-33.9-14.1-18.7-18.7-18.7-49.2 0-67.9l64-64c9.1-9.1 21.1-14.1 33.9-14.1 2.8 0 5.6.3 8.4.7l-27.8 27.8c-5.2 5.2-8.1 12.1-8.1 19.4s2.9 14.3 8.1 19.4c5.2 5.2 12.1 8.1 19.4 8.1s14.3-2.9 19.4-8.1l27.8-27.8c2.7 15.2-1.8 31.1-13.3 42.5zm224-224l-64 64c-9 9.1-21.1 14.1-33.9 14.1-2.8 0-5.6-.3-8.4-.7l27.8-27.8c5.2-5.2 8.1-12.1 8.1-19.4s-2.9-14.3-8.1-19.4c-5.2-5.2-12.1-8.1-19.4-8.1s-14.3 2.9-19.4 8.1l-27.8 27.8c-2.6-14.9 1.8-30.8 13.3-42.3l64-64C375.1 69 387.2 64 400 64s24.9 5 33.9 14.1c18.8 18.7 18.8 49.1 0 67.8z'/%3e%3c/svg%3e");
$bg-icon-clock: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M288 32H160l160 160H174.872C152.74 153.742 111.377 128 64 128H0v256h64c47.377 0 88.74-25.742 110.872-64H320L160 480h128l224-224L288 32z'/%3e%3c/svg%3e");
$bg-icon-database: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M256 0C148.6 0 56.6 66.2 18.6 160H64c28.4 0 54 12.4 71.5 32H256l-96-96h128l160 160-160 160H160l96-96H135.5C118 339.6 92.4 352 64 352H18.6c38 93.8 129.9 160 237.4 160 141.4 0 256-114.6 256-256S397.4 0 256 0z'/%3e%3c/svg%3e");
$bg-icon-database-query: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M96 0C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h32V0H96zM192 0h128v512H192zM416 0h-32v352h128V96c0-52.8-43.2-96-96-96z'/%3e%3c/svg%3e");
$bg-icon-dataset: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256 256-114.6 256-256S397.4 0 256 0zm135 345c-6.4 11.1-18.3 18-31.2 18-6.3 0-12.5-1.7-18-4.8l-110.9-64-.1-.1c-.4-.2-.8-.5-1.2-.7l-.4-.3-.9-.6-.6-.5-.6-.5-.9-.7-.3-.3c-.4-.3-.7-.6-1.1-.9-2.5-2.3-4.7-5-6.5-7.9-.1-.2-.3-.5-.4-.7s-.3-.5-.4-.7c-1.6-3-2.9-6.2-3.6-9.6v-.1c-.1-.5-.2-.9-.3-1.4 0-.1 0-.3-.1-.4-.1-.3-.1-.7-.1-1.1s-.1-.5-.1-.8 0-.5-.1-.8-.1-.8-.1-1.1v-.5-1.4V81c0-19.9 16.1-36 36-36s36 16.1 36 36v161.2l92.9 53.6c17.1 10 22.9 32 13 49.2z'/%3e%3c/svg%3e");
$bg-icon-datatable: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M256 256C114.615 256 0 213.019 0 160v256c0 53.019 114.615 96 256 96s256-42.981 256-96V160c0 53.019-114.615 96-256 96z'/%3e%3cellipse fill='%23000000' cx='256' cy='96' rx='256' ry='96'/%3e%3c/svg%3e");
$bg-icon-dictionary: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M341.76 409.643C316.369 423.871 287.118 432 256 432c-97.047 0-176-78.953-176-176S158.953 80 256 80s176 78.953 176 176c0 31.118-8.129 60.369-22.357 85.76l95.846 95.846C509.747 430.661 512 423.429 512 416V96c0-53.019-114.615-96-256-96S0 42.981 0 96v320c0 53.019 114.615 96 256 96 63.055 0 120.774-8.554 165.388-22.73l-79.628-79.627z'/%3e%3cpath fill='%23000000' d='M176 256c0 44.112 35.888 80 80 80s80-35.888 80-80-35.888-80-80-80-80 35.888-80 80z'/%3e%3c/svg%3e");
$bg-icon-folder: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 96H288l-54.6-54.6-18.7-18.7C202.2 10.2 177.6 0 160 0H32C14.4 0 0 14.4 0 32v192c0-35.2 28.8-64 64-64h384c35.2 0 64 28.8 64 64v-64c0-35.2-28.8-64-64-64zM448 224H64c-35.2 0-64 28.8-64 64v160c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V288c0-35.2-28.8-64-64-64zM160 448H96V288h64v160zm128 0h-64V288h64v160zm128 0h-64V288h64v160z'/%3e%3c/svg%3e");
$bg-icon-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M256 256C114.6 256 0 213 0 160v256c0 53 114.6 96 256 96s256-43 256-96V160c0 53-114.6 96-256 96zm192 31.5v128c-18.3 7.8-39.9 14.4-64 19.7v-128c24.1-5.3 45.7-11.9 64-19.7zm-320 19.7v128c-24.1-5.2-45.7-11.9-64-19.7v-128c18.3 7.8 39.9 14.4 64 19.7zM192 445V317c20.5 2 41.9 3 64 3s43.5-1.1 64-3v128c-20.5 2-41.9 3-64 3s-43.5-1.1-64-3z'/%3e%3cellipse fill='%23000000' cx='256' cy='96' rx='256' ry='96'/%3e%3c/svg%3e");
$bg-icon-layout: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M416 320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96v160l-64-32-64 32V0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96v-96c0 52.8-43.2 96-96 96H96v-96h320z'/%3e%3c/svg%3e");
$bg-icon-object: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 96H288l-54.6-54.6-18.7-18.7C202.2 10.2 177.6 0 160 0H32C14.4 0 0 14.4 0 32v192c0-35.2 28.8-64 64-64h384c35.2 0 64 28.8 64 64v-64c0-35.2-28.8-64-64-64zM448 224H64c-35.2 0-64 28.8-64 64v160c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V288c0-35.2-28.8-64-64-64z'/%3e%3c/svg%3e");
$bg-icon-object-unknown: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 0H64C28.8 0 0 28.8 0 64v384c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V64c0-35.2-28.8-64-64-64zm0 448H64V64h384v384z'/%3e%3cpath fill='%23000000' d='M160 128l-64 64v224h320V256l-64-64-64 64z'/%3e%3c/svg%3e");
$bg-icon-packet: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M224 0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h128V0zM416 0H288v288.832h224V96c0-52.8-43.2-96-96-96zM288 512h128c52.8 0 96-43.2 96-96v-64.832H288V512z'/%3e%3c/svg%3e");
$bg-icon-page: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='none' d='M256 96L76.8 208 256 320l179.2-112z'/%3e%3cpath fill='%23000000' d='M256 512l256-160V160L255.99 0 0 160v192l256 160zm0-416l179.2 112L256 320 76.8 208 256 96z'/%3e%3c/svg%3e");
$bg-icon-plot-overlay: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M255-1L-1 159v192l256 160 256-160V159L255-1zm37.7 430.6c-10.6 10.4-23 15.4-38 15.4-15.6 0-28.1-4.9-38.1-14.8-10-10-14.8-22.4-14.8-38.1 0-15.2 5.1-27.6 15.5-38.1s22.6-15.6 37.4-15.6c14.8 0 27.1 5.2 37.8 16 10.7 10.8 15.9 23.2 15.9 38-.1 14.5-5.4 27-15.7 37.2zm26.4-156.3c-11.8 5.9-18.7 11-21.7 16.2-1.8 3.1-3 7.4-3.7 13.4v20.5H213v-22.1c0-20.1 2.2-34.9 6.5-44 4-8.6 11.3-15.1 22.4-20l17.4-7.7c16-7.1 24.1-17.6 24.1-31.4 0-8-3-15.2-8.6-20.9-5.6-5.6-12.8-8.6-20.8-8.6-12 0-27.2 5-31.4 28.7l-1.1 6.1H148l.7-8.1c2-22.3 8.5-41.2 19.4-56.1 9.8-13.5 22.8-24.3 38.5-32.3 15.7-8 32.3-12 49.1-12 30.3 0 55.1 9.7 75.7 29.8 20.6 20 30.6 44 30.6 73.6 0 35.4-14.4 60.7-42.9 74.9z'/%3e%3c/svg%3e");
$bg-icon-plot-stacked: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='none' d='M256 96L76.8 208 256 320l179.2-112z'/%3e%3cpath fill='%23000000' d='M256 0L0 160v256c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V160L256 0zm0 96l179.2 112L256 320 76.8 208 256 96z'/%3e%3c/svg%3e");
$bg-icon-session: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M352 256c-52.8 0-96-43.2-96-96V0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V256H352z'/%3e%3cpath fill='%23000000' d='M384 192h128L320 0v128c0 35.2 28.8 64 64 64z'/%3e%3c/svg%3e");
$bg-icon-tabular: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M415 0H97C43.65 0 0 43.65 0 97v203.41c7.09 9.32 12.83 14.17 16 15.42 7.14-2.81 27.22-23.77 46.48-73C83.71 188.64 120.64 124 176 124c26.2 0 50.71 14.58 72.85 43.34 18.67 24.25 32.42 54.46 40.67 75.54 19.26 49.19 39.34 70.15 46.48 73 7.14-2.81 27.22-23.77 46.48-73C403.71 188.64 440.64 124 496 124a69.55 69.55 0 0 1 16 1.87V97c0-53.35-43.65-97-97-97z'/%3e%3cpath fill='%23000000' d='M496 196.17c-7.14 2.81-27.22 23.76-46.48 73C428.29 323.36 391.36 388 336 388c-26.2 0-50.71-14.58-72.85-43.34-18.67-24.25-32.42-54.46-40.67-75.54-19.26-49.19-39.34-70.15-46.48-73-7.14 2.81-27.22 23.76-46.48 73C108.29 323.36 71.36 388 16 388a69.56 69.56 0 0 1-16-1.87V415c0 53.35 43.65 97 97 97h318c53.35 0 97-43.65 97-97V211.59c-7.09-9.32-12.83-14.17-16-15.42z'/%3e%3c/svg%3e");
$bg-icon-tabular-lad: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M44.8 156c12.49 0 24.48-13.26 42.76-35.09 22.71-27.14 51-60.91 98-60.91 22.32 0 43.31 7.73 62.4 23 14.34 11.45 25.58 25.21 36.46 38.53C303.63 145 314 156 326.4 156H512V97c0-53.35-43.65-97-97-97H97C43.65 0 0 43.65 0 97v59h44.8z'/%3e%3cpath fill='%23000000' d='M264.75 205.2c-14.12-11.32-25.26-25-36-38.14C211 145.32 199.37 132 185.6 132c-12.53 0-24.54 13.27-42.83 35.12-22.7 27.12-51 60.88-98 60.88H0v56h185.6c22 0 42.77 7.67 61.65 22.8 14.12 11.32 25.26 25 36 38.14C301 366.68 312.63 380 326.4 380c12.53 0 24.54-13.27 42.83-35.12 22.7-27.12 51-60.88 98-60.88H512v-56H326.4c-22.03 0-42.77-7.67-61.65-22.8z'/%3e%3cpath fill='%23000000' d='M467.2 356c-12.49 0-24.48 13.26-42.76 35.09-22.71 27.14-51 60.91-98 60.91-22.32 0-43.31-7.73-62.4-23-14.34-11.45-25.58-25.21-36.46-38.53C208.37 367 198 356 185.6 356H0v59c0 53.35 43.65 97 97 97h318c53.35 0 97-43.65 97-97v-59h-44.8z'/%3e%3c/svg%3e");
$bg-icon-tabular-lad-set: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M317.8 262.2c3.3 2.1 6.6 4.3 9.6 6.8l60.2 48.2c14.8 11.9 41.9 11.9 56.7 0l67.6-54c.1-2.4.1-4.7.1-7.1 0-26.1-3.9-51.2-11.1-74.9L423.5 243c-29.1 23.3-70.1 29.6-105.7 19.2zM124.3 317.1l60.2-48.2c29-23.2 70-29.6 105.6-19.2-3.3-2.1-6.6-4.3-9.6-6.8l-60.2-48.2c-14.8-11.9-41.9-11.9-56.7 0L103.5 243c-20 16-45.7 24-71.5 24-10.8 0-21.5-1.4-31.9-4.2v.8c2.5 1.7 5 3.4 7.3 5.3l60.2 48.2c14.9 11.9 41.9 11.9 56.7 0z'/%3e%3cpath fill='%23000000' d='M60.3 189.1l60.2-48.2c40.1-32.1 102.8-32.1 142.9 0l60.2 48.2c14.8 11.9 41.9 11.9 56.7 0l90.5-72.4C425.2 46.5 346 0 256 0 136.7 0 36.4 81.6 8 192.1c15.4 8.8 38.9 7.8 52.3-3zM344.5 371l-60.2-48.2c-14.8-11.9-41.9-11.9-56.7 0L167.5 371c-20 16-45.7 24-71.5 24-23.9 0-47.7-6.9-67.1-20.7C71.7 456.1 157.3 512 256 512s184.3-55.9 227.1-137.7c-40.2 28.7-99.9 27.6-138.6-3.3z'/%3e%3c/svg%3e");
$bg-icon-tabular-realtime: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 0H64C28.8 0 0 28.8 0 64v384c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V64c0-35.2-28.8-64-64-64zM320 224H192v-96h128v96zm-128 32h128v96H192v-96zm-32 96H32v-96h128v96zm0-224v96H32v-96h128zM64 480c-8.5 0-16.5-3.3-22.6-9.4S32 456.5 32 448v-64h128v96H64zm128 0v-96h128v96H192zm288-32c0 8.5-3.3 16.5-9.4 22.6S456.5 480 448 480h-96v-96h128v64zm0-96H352v-96h128v96zm0-128H352v-96h128v96z'/%3e%3c/svg%3e");
$bg-icon-tabular-scrolling: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 0H64C28.7.1.1 28.7 0 64v384c.1 35.3 28.7 63.9 64 64h384c35.3-.1 63.9-28.7 64-64V64c-.1-35.3-28.7-63.9-64-64zM32 128h128v96H32v-96zm0 128h128v96H32v-96zm32 224c-17.6-.1-31.9-14.4-32-32v-64h128v96H64zm128 0v-96h128v96H192zm288-32c-.1 17.6-14.4 31.9-32 32h-96v-96h128v64zm0-192v96H192v-96h32v-32h-32v-96h288v96h-32v32h32z'/%3e%3cpath fill='%23000000' d='M391.2 273.7L336 246.1V160c0-8.8-7.2-16-16-16s-16 7.2-16 16v105.9l72.8 36.4c7.9 4 17.5.8 21.5-7.2 4-7.8.8-17.5-7.1-21.4z'/%3e%3c/svg%3e");
$bg-icon-telemetry: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M64 384V96c-35.3.1-63.9 28.7-64 64v288c.1 35.3 28.7 63.9 64 64h288c35.3-.1 63.9-28.7 64-64H128c-35.3-.1-63.9-28.7-64-64z'/%3e%3cpath fill='%23000000' d='M448 0H160c-35.3.1-63.9 28.7-64 64v288c.1 35.3 28.7 63.9 64 64h288c35.3-.1 63.9-28.7 64-64V64c-.1-35.3-28.7-63.9-64-64zM128 96h96v64h-96V96zm0 96h96v96h-96v-96zm32 192c-17.6-.1-31.9-14.4-32-32v-32h96v64h-64zm96 0v-64h96v64h-96zm224-32c-.1 17.6-14.4 31.9-32 32h-64v-64h96v32zm0-64H256V96h224v192z'/%3e%3cpath fill='%23000000' d='M416 240c8.8 0 16-7.2 16-16 0-6.9-4.4-13-10.9-15.2L384 196.5V144c0-8.8-7.2-16-16-16s-16 7.2-16 16v75.5l58.9 19.6c1.7.6 3.4.9 5.1.9z'/%3e%3c/svg%3e");
$bg-icon-clock: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256 256-114.6 256-256S397.4 0 256 0zm135 345c-6.4 11.1-18.3 18-31.2 18-6.3 0-12.5-1.7-18-4.8l-110.9-64-.1-.1c-.4-.2-.8-.5-1.2-.7l-.4-.3-.9-.6-.6-.5-.6-.5-.9-.7-.3-.3c-.4-.3-.7-.6-1.1-.9-2.5-2.3-4.7-5-6.5-7.9-.1-.2-.3-.5-.4-.7s-.3-.5-.4-.7c-1.6-3-2.9-6.2-3.6-9.6v-.1c-.1-.5-.2-.9-.3-1.4 0-.1 0-.3-.1-.4-.1-.3-.1-.7-.1-1.1s-.1-.5-.1-.8 0-.5-.1-.8-.1-.8-.1-1.1v-.5-1.4V81c0-19.9 16.1-36 36-36s36 16.1 36 36v161.2l92.9 53.6c17.1 10 22.9 32 13 49.2z'/%3e%3c/svg%3e");
$bg-icon-database: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M256 256C114.615 256 0 213.019 0 160v256c0 53.019 114.615 96 256 96s256-42.981 256-96V160c0 53.019-114.615 96-256 96z'/%3e%3cellipse fill='%23000000' cx='256' cy='96' rx='256' ry='96'/%3e%3c/svg%3e");
$bg-icon-database-query: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M341.76 409.643C316.369 423.871 287.118 432 256 432c-97.047 0-176-78.953-176-176S158.953 80 256 80s176 78.953 176 176c0 31.118-8.129 60.369-22.357 85.76l95.846 95.846C509.747 430.661 512 423.429 512 416V96c0-53.019-114.615-96-256-96S0 42.981 0 96v320c0 53.019 114.615 96 256 96 63.055 0 120.774-8.554 165.388-22.73l-79.628-79.627z'/%3e%3cpath fill='%23000000' d='M176 256c0 44.112 35.888 80 80 80s80-35.888 80-80-35.888-80-80-80-80 35.888-80 80z'/%3e%3c/svg%3e");
$bg-icon-dataset: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 96H288l-54.6-54.6-18.7-18.7C202.2 10.2 177.6 0 160 0H32C14.4 0 0 14.4 0 32v192c0-35.2 28.8-64 64-64h384c35.2 0 64 28.8 64 64v-64c0-35.2-28.8-64-64-64zM448 224H64c-35.2 0-64 28.8-64 64v160c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V288c0-35.2-28.8-64-64-64zM160 448H96V288h64v160zm128 0h-64V288h64v160zm128 0h-64V288h64v160z'/%3e%3c/svg%3e");
$bg-icon-datatable: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M256 256C114.6 256 0 213 0 160v256c0 53 114.6 96 256 96s256-43 256-96V160c0 53-114.6 96-256 96zm192 31.5v128c-18.3 7.8-39.9 14.4-64 19.7v-128c24.1-5.3 45.7-11.9 64-19.7zm-320 19.7v128c-24.1-5.2-45.7-11.9-64-19.7v-128c18.3 7.8 39.9 14.4 64 19.7zM192 445V317c20.5 2 41.9 3 64 3s43.5-1.1 64-3v128c-20.5 2-41.9 3-64 3s-43.5-1.1-64-3z'/%3e%3cellipse fill='%23000000' cx='256' cy='96' rx='256' ry='96'/%3e%3c/svg%3e");
$bg-icon-dictionary: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M416 320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96v160l-64-32-64 32V0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96v-96c0 52.8-43.2 96-96 96H96v-96h320z'/%3e%3c/svg%3e");
$bg-icon-folder: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 96H288l-54.6-54.6-18.7-18.7C202.2 10.2 177.6 0 160 0H32C14.4 0 0 14.4 0 32v192c0-35.2 28.8-64 64-64h384c35.2 0 64 28.8 64 64v-64c0-35.2-28.8-64-64-64zM448 224H64c-35.2 0-64 28.8-64 64v160c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V288c0-35.2-28.8-64-64-64z'/%3e%3c/svg%3e");
$bg-icon-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 0H64C28.8 0 0 28.8 0 64v384c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V64c0-35.2-28.8-64-64-64zm0 448H64V64h384v384z'/%3e%3cpath fill='%23000000' d='M160 128l-64 64v224h320V256l-64-64-64 64z'/%3e%3c/svg%3e");
$bg-icon-layout: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M224 0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h128V0zM416 0H288v288.832h224V96c0-52.8-43.2-96-96-96zM288 512h128c52.8 0 96-43.2 96-96v-64.832H288V512z'/%3e%3c/svg%3e");
$bg-icon-object: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='none' d='M256 96L76.8 208 256 320l179.2-112z'/%3e%3cpath fill='%23000000' d='M256 512l256-160V160L255.99 0 0 160v192l256 160zm0-416l179.2 112L256 320 76.8 208 256 96z'/%3e%3c/svg%3e");
$bg-icon-object-unknown: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M255-1L-1 159v192l256 160 256-160V159L255-1zm37.7 430.6c-10.6 10.4-23 15.4-38 15.4-15.6 0-28.1-4.9-38.1-14.8-10-10-14.8-22.4-14.8-38.1 0-15.2 5.1-27.6 15.5-38.1s22.6-15.6 37.4-15.6c14.8 0 27.1 5.2 37.8 16 10.7 10.8 15.9 23.2 15.9 38-.1 14.5-5.4 27-15.7 37.2zm26.4-156.3c-11.8 5.9-18.7 11-21.7 16.2-1.8 3.1-3 7.4-3.7 13.4v20.5H213v-22.1c0-20.1 2.2-34.9 6.5-44 4-8.6 11.3-15.1 22.4-20l17.4-7.7c16-7.1 24.1-17.6 24.1-31.4 0-8-3-15.2-8.6-20.9-5.6-5.6-12.8-8.6-20.8-8.6-12 0-27.2 5-31.4 28.7l-1.1 6.1H148l.7-8.1c2-22.3 8.5-41.2 19.4-56.1 9.8-13.5 22.8-24.3 38.5-32.3 15.7-8 32.3-12 49.1-12 30.3 0 55.1 9.7 75.7 29.8 20.6 20 30.6 44 30.6 73.6 0 35.4-14.4 60.7-42.9 74.9z'/%3e%3c/svg%3e");
$bg-icon-packet: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='none' d='M256 96L76.8 208 256 320l179.2-112z'/%3e%3cpath fill='%23000000' d='M256 0L0 160v256c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V160L256 0zm0 96l179.2 112L256 320 76.8 208 256 96z'/%3e%3c/svg%3e");
$bg-icon-page: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M352 256c-52.8 0-96-43.2-96-96V0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V256H352z'/%3e%3cpath fill='%23000000' d='M384 192h128L320 0v128c0 35.2 28.8 64 64 64z'/%3e%3c/svg%3e");
$bg-icon-plot-overlay: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M415 0H97C43.65 0 0 43.65 0 97v203.41c7.09 9.32 12.83 14.17 16 15.42 7.14-2.81 27.22-23.77 46.48-73C83.71 188.64 120.64 124 176 124c26.2 0 50.71 14.58 72.85 43.34 18.67 24.25 32.42 54.46 40.67 75.54 19.26 49.19 39.34 70.15 46.48 73 7.14-2.81 27.22-23.77 46.48-73C403.71 188.64 440.64 124 496 124a69.55 69.55 0 0 1 16 1.87V97c0-53.35-43.65-97-97-97z'/%3e%3cpath fill='%23000000' d='M496 196.17c-7.14 2.81-27.22 23.76-46.48 73C428.29 323.36 391.36 388 336 388c-26.2 0-50.71-14.58-72.85-43.34-18.67-24.25-32.42-54.46-40.67-75.54-19.26-49.19-39.34-70.15-46.48-73-7.14 2.81-27.22 23.76-46.48 73C108.29 323.36 71.36 388 16 388a69.56 69.56 0 0 1-16-1.87V415c0 53.35 43.65 97 97 97h318c53.35 0 97-43.65 97-97V211.59c-7.09-9.32-12.83-14.17-16-15.42z'/%3e%3c/svg%3e");
$bg-icon-plot-stacked: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M44.8 156c12.49 0 24.48-13.26 42.76-35.09 22.71-27.14 51-60.91 98-60.91 22.32 0 43.31 7.73 62.4 23 14.34 11.45 25.58 25.21 36.46 38.53C303.63 145 314 156 326.4 156H512V97c0-53.35-43.65-97-97-97H97C43.65 0 0 43.65 0 97v59h44.8z'/%3e%3cpath fill='%23000000' d='M264.75 205.2c-14.12-11.32-25.26-25-36-38.14C211 145.32 199.37 132 185.6 132c-12.53 0-24.54 13.27-42.83 35.12-22.7 27.12-51 60.88-98 60.88H0v56h185.6c22 0 42.77 7.67 61.65 22.8 14.12 11.32 25.26 25 36 38.14C301 366.68 312.63 380 326.4 380c12.53 0 24.54-13.27 42.83-35.12 22.7-27.12 51-60.88 98-60.88H512v-56H326.4c-22.03 0-42.77-7.67-61.65-22.8z'/%3e%3cpath fill='%23000000' d='M467.2 356c-12.49 0-24.48 13.26-42.76 35.09-22.71 27.14-51 60.91-98 60.91-22.32 0-43.31-7.73-62.4-23-14.34-11.45-25.58-25.21-36.46-38.53C208.37 367 198 356 185.6 356H0v59c0 53.35 43.65 97 97 97h318c53.35 0 97-43.65 97-97v-59h-44.8z'/%3e%3c/svg%3e");
$bg-icon-session: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M317.8 262.2c3.3 2.1 6.6 4.3 9.6 6.8l60.2 48.2c14.8 11.9 41.9 11.9 56.7 0l67.6-54c.1-2.4.1-4.7.1-7.1 0-26.1-3.9-51.2-11.1-74.9L423.5 243c-29.1 23.3-70.1 29.6-105.7 19.2zM124.3 317.1l60.2-48.2c29-23.2 70-29.6 105.6-19.2-3.3-2.1-6.6-4.3-9.6-6.8l-60.2-48.2c-14.8-11.9-41.9-11.9-56.7 0L103.5 243c-20 16-45.7 24-71.5 24-10.8 0-21.5-1.4-31.9-4.2v.8c2.5 1.7 5 3.4 7.3 5.3l60.2 48.2c14.9 11.9 41.9 11.9 56.7 0z'/%3e%3cpath fill='%23000000' d='M60.3 189.1l60.2-48.2c40.1-32.1 102.8-32.1 142.9 0l60.2 48.2c14.8 11.9 41.9 11.9 56.7 0l90.5-72.4C425.2 46.5 346 0 256 0 136.7 0 36.4 81.6 8 192.1c15.4 8.8 38.9 7.8 52.3-3zM344.5 371l-60.2-48.2c-14.8-11.9-41.9-11.9-56.7 0L167.5 371c-20 16-45.7 24-71.5 24-23.9 0-47.7-6.9-67.1-20.7C71.7 456.1 157.3 512 256 512s184.3-55.9 227.1-137.7c-40.2 28.7-99.9 27.6-138.6-3.3z'/%3e%3c/svg%3e");
$bg-icon-tabular: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 0H64C28.8 0 0 28.8 0 64v384c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V64c0-35.2-28.8-64-64-64zM320 224H192v-96h128v96zm-128 32h128v96H192v-96zm-32 96H32v-96h128v96zm0-224v96H32v-96h128zM64 480c-8.5 0-16.5-3.3-22.6-9.4S32 456.5 32 448v-64h128v96H64zm128 0v-96h128v96H192zm288-32c0 8.5-3.3 16.5-9.4 22.6S456.5 480 448 480h-96v-96h128v64zm0-96H352v-96h128v96zm0-128H352v-96h128v96z'/%3e%3c/svg%3e");
$bg-icon-tabular-lad: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 0H64C28.7.1.1 28.7 0 64v384c.1 35.3 28.7 63.9 64 64h384c35.3-.1 63.9-28.7 64-64V64c-.1-35.3-28.7-63.9-64-64zM32 128h128v96H32v-96zm0 128h128v96H32v-96zm32 224c-17.6-.1-31.9-14.4-32-32v-64h128v96H64zm128 0v-96h128v96H192zm288-32c-.1 17.6-14.4 31.9-32 32h-96v-96h128v64zm0-192v96H192v-96h32v-32h-32v-96h288v96h-32v32h32z'/%3e%3cpath fill='%23000000' d='M391.2 273.7L336 246.1V160c0-8.8-7.2-16-16-16s-16 7.2-16 16v105.9l72.8 36.4c7.9 4 17.5.8 21.5-7.2 4-7.8.8-17.5-7.1-21.4z'/%3e%3c/svg%3e");
$bg-icon-tabular-lad-set: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M64 384V96c-35.3.1-63.9 28.7-64 64v288c.1 35.3 28.7 63.9 64 64h288c35.3-.1 63.9-28.7 64-64H128c-35.3-.1-63.9-28.7-64-64z'/%3e%3cpath fill='%23000000' d='M448 0H160c-35.3.1-63.9 28.7-64 64v288c.1 35.3 28.7 63.9 64 64h288c35.3-.1 63.9-28.7 64-64V64c-.1-35.3-28.7-63.9-64-64zM128 96h96v64h-96V96zm0 96h96v96h-96v-96zm32 192c-17.6-.1-31.9-14.4-32-32v-32h96v64h-64zm96 0v-64h96v64h-96zm224-32c-.1 17.6-14.4 31.9-32 32h-64v-64h96v32zm0-64H256V96h224v192z'/%3e%3cpath fill='%23000000' d='M416 240c8.8 0 16-7.2 16-16 0-6.9-4.4-13-10.9-15.2L384 196.5V144c0-8.8-7.2-16-16-16s-16 7.2-16 16v75.5l58.9 19.6c1.7.6 3.4.9 5.1.9z'/%3e%3c/svg%3e");
$bg-icon-tabular-scrolling: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M32 0C14.4 0 0 14.4 0 32v96h224V0H32zM512 128V32c0-17.6-14.4-32-32-32H288v128h224zM0 192v96c0 17.6 14.4 32 32 32h192V192H0zM480 320c17.6 0 32-14.4 32-32v-96H288v128h192zM256 512L128 384h256z'/%3e%3c/svg%3e");
$bg-icon-telemetry: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M16 315.83c7.14-2.81 27.22-23.77 46.48-73C83.71 188.64 120.64 124 176 124c26.2 0 50.71 14.58 72.85 43.34 18.67 24.25 32.42 54.46 40.67 75.54 19.26 49.19 39.34 70.15 46.48 73 7.14-2.81 27.22-23.77 46.48-73 18.7-47.75 49.57-103.57 94.47-116.23A255.87 255.87 0 0 0 256 0C114.62 0 0 114.62 0 256a257.18 257.18 0 0 0 5 50.52c4.77 5.39 8.61 8.37 11 9.31z'/%3e%3cpath fill='%23000000' d='M496 196.17c-7.14 2.81-27.22 23.76-46.48 73C428.29 323.36 391.36 388 336 388c-26.2 0-50.71-14.58-72.85-43.34-18.67-24.25-32.42-54.46-40.67-75.54-19.26-49.19-39.34-70.15-46.48-73-7.14 2.81-27.22 23.76-46.48 73-18.7 47.75-49.57 103.57-94.47 116.23A255.87 255.87 0 0 0 256 512c141.38 0 256-114.62 256-256a257.18 257.18 0 0 0-5-50.52c-4.77-5.39-8.61-8.37-11-9.31z'/%3e%3c/svg%3e");
$bg-icon-timeline: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M416 0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96ZM64 160V96h128v64Zm64 64h192v64H128Zm320 192H224v-64h224Zm0-128h-64v-64h64Zm0-128H256V96h192Z'/%3e%3c/svg%3e");
$bg-icon-timer: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M32 0C14.4 0 0 14.4 0 32v96h224V0H32zM512 128V32c0-17.6-14.4-32-32-32H288v128h224zM0 192v96c0 17.6 14.4 32 32 32h192V192H0zM480 320c17.6 0 32-14.4 32-32v-96H288v128h192zM256 512L128 384h256z'/%3e%3c/svg%3e");
$bg-icon-topic: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M16 315.83c7.14-2.81 27.22-23.77 46.48-73C83.71 188.64 120.64 124 176 124c26.2 0 50.71 14.58 72.85 43.34 18.67 24.25 32.42 54.46 40.67 75.54 19.26 49.19 39.34 70.15 46.48 73 7.14-2.81 27.22-23.77 46.48-73 18.7-47.75 49.57-103.57 94.47-116.23A255.87 255.87 0 0 0 256 0C114.62 0 0 114.62 0 256a257.18 257.18 0 0 0 5 50.52c4.77 5.39 8.61 8.37 11 9.31z'/%3e%3cpath fill='%23000000' d='M496 196.17c-7.14 2.81-27.22 23.76-46.48 73C428.29 323.36 391.36 388 336 388c-26.2 0-50.71-14.58-72.85-43.34-18.67-24.25-32.42-54.46-40.67-75.54-19.26-49.19-39.34-70.15-46.48-73-7.14 2.81-27.22 23.76-46.48 73-18.7 47.75-49.57 103.57-94.47 116.23A255.87 255.87 0 0 0 256 512c141.38 0 256-114.62 256-256a257.18 257.18 0 0 0-5-50.52c-4.77-5.39-8.61-8.37-11-9.31z'/%3e%3c/svg%3e");
$bg-icon-timer: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M288 73.3V32.01a32 32 0 0 0-32-32h-64a32 32 0 0 0-32 32V73.3C67.48 100.84 0 186.54 0 288.01c0 123.71 100.29 224 224 224s224-100.29 224-224c0-101.48-67.5-187.2-160-214.71zm-54 224.71l-131.88 105.5A167.4 167.4 0 0 1 56 288.01c0-92.64 75.36-168 168-168 3.36 0 6.69.11 10 .31v177.69z'/%3e%3c/svg%3e");
$bg-icon-box-with-dashed-lines: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M0 192h64v128H0zM64 64.11l.11-.11H160V0H64A64.19 64.19 0 0 0 0 64v96h64V64.11zM64 447.89V352H0v96a64.19 64.19 0 0 0 64 64h96v-64H64.11zM192 0h128v64H192zM448 447.89l-.11.11H352v64h96a64.19 64.19 0 0 0 64-64v-96h-64v95.89zM448 0h-96v64h95.89l.11.11V160h64V64a64.19 64.19 0 0 0-64-64zM448 192h64v128h-64zM192 448h128v64H192zM128 128h256v256H128z'/%3e%3c/svg%3e");
$bg-icon-summary-widget: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M128 128h192v64H128zM192 224h192v64H192zM160 320h192v64H160z'/%3e%3cpath fill='%23000000' d='M416 0h-64v96h63.8c.1 0 .1.1.2.2v319.7c0 .1-.1.1-.2.2H352v96h64c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96zM96 415.8V96.2c0-.1.1-.1.2-.2H160V0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h64v-96H96.2c-.1 0-.2-.1-.2-.2z'/%3e%3c/svg%3e");
$bg-icon-notebook: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M288 73.3V32.01a32 32 0 0 0-32-32h-64a32 32 0 0 0-32 32V73.3C67.48 100.84 0 186.54 0 288.01c0 123.71 100.29 224 224 224s224-100.29 224-224c0-101.48-67.5-187.2-160-214.71zm-54 224.71l-131.88 105.5A167.4 167.4 0 0 1 56 288.01c0-92.64 75.36-168 168-168 3.36 0 6.69.11 10 .31v177.69z'/%3e%3c/svg%3e");
$bg-icon-summary-widget: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M416 0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96zM256 384L64 256l192-128 192 128z'/%3e%3c/svg%3e");
$bg-icon-notebook: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' xml:space='preserve'%3e%3cpath d='M448 55.4c0-39.9-27.7-63.7-61.5-52.7L0 128h448V55.4zM448 160H0v288c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64zm-32 256H224V256h192v160z'/%3e%3c/svg%3e");
$bg-icon-tabs-view: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M0 448a64.2 64.2 0 0 0 64 64h384a64.2 64.2 0 0 0 64-64V144H256L230.9 31.2C227.1 14.1 209.6 0 192 0H64A64.2 64.2 0 0 0 0 64zm416-64H96V256h320z'/%3e%3cpath d='M240 0c17.6 0 35.1 14.1 38.9 31.2l18 80.8H512V64a64.2 64.2 0 0 0-64-64z'/%3e%3c/svg%3e");
$bg-icon-flexible-layout: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M0 416c0 52.8 43.2 96 96 96h32V224H0zM0 96v64h128V0H96C43.2 0 0 43.2 0 96zM384 512h32c52.8 0 96-43.2 96-96v-64H384zM192 0h128v512H192zM416 0h-32v288h128V96c0-52.8-43.2-96-96-96z'/%3e%3c/svg%3e");
$bg-icon-generator-telemetry: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M76 236.9c5.4-2.1 20.4-17.8 34.9-54.7C126.8 141.5 154.5 93 196 93c19.7 0 38 10.9 54.6 32.5 14 18.2 24.4 40.8 30.5 56.7 14.5 36.9 29.5 52.6 34.9 54.7 5.4-2.1 20.4-17.8 34.9-54.7S388 104.5 421.7 95A192 192 0 0 0 256 0C150 0 64 86 64 192a197.2 197.2 0 0 0 3.7 37.9c3.6 4 6.5 6.3 8.3 7zM442.3 238.5A192.9 192.9 0 0 0 448 192a197.2 197.2 0 0 0-3.7-37.9c-3.6-4-6.5-6.3-8.3-7-5.4 2.1-20.4 17.8-34.9 54.7-10.9 27.9-27.3 59.5-50 76.6z'/%3e%3cpath d='M256 320l67.5-29.5a60.3 60.3 0 0 1-7.5.5c-19.7 0-38-10.9-54.6-32.5-14-18.2-24.4-40.8-30.5-56.7-14.5-36.9-29.5-52.6-34.9-54.7-5.4 2.1-20.4 17.8-34.9 54.7-8.2 21.1-19.6 44.2-34.4 61.6z'/%3e%3cpath d='M512 240L256 352 0 240v160l256 112 256-112V240z'/%3e%3c/svg%3e");
@ -325,5 +323,5 @@ $bg-icon-bar-chart: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://w
$bg-icon-map: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 32.7 384 64v448l64-31.3c35.2-17.21 64-60.1 64-95.3v-320c0-35.2-28.8-49.91-64-32.7ZM160 456l193.6 48.4v-448L160 8v448zM129.6.4 128 0 64 31.3C28.8 48.51 0 91.4 0 126.6v320c0 35.2 28.8 49.91 64 32.7l64-31.3 1.6.4Z'/%3e%3c/svg%3e");
$bg-icon-plan: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cg data-name='Layer 1'%3e%3cpath fill='%23000000' d='M128 96V64a64.19 64.19 0 0 1 64-64h128a64.19 64.19 0 0 1 64 64v32Z'/%3e%3cpath fill='%23000000' d='M416 64v64H96V64c-52.8 0-96 43.2-96 96v256c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V160c0-52.8-43.2-96-96-96ZM64 288v-64h128v64Zm256 128H128v-64h192Zm128 0h-64v-64h64Zm0-128H256v-64h192Z'/%3e%3c/g%3e%3c/g%3e%3c/svg%3e");
$bg-icon-timelist: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cpath d='M448 0H64A64.19 64.19 0 0 0 0 64v384a64.19 64.19 0 0 0 64 64h384a64.19 64.19 0 0 0 64-64V64a64.19 64.19 0 0 0-64-64ZM213.47 266.73a24 24 0 0 1-32.2 10.74L104 238.83V128a24 24 0 0 1 48 0v81.17l50.73 25.36a24 24 0 0 1 10.74 32.2ZM448 448H288v-64h160Zm0-96H288v-64h160Zm0-96H288v-64h160Zm0-96H288V96h160Z' data-name='Layer 1'/%3e%3c/g%3e%3c/svg%3e");
$bg-icon-notebook-shift-log: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M448 55.36c0-39.95-27.69-63.66-61.54-52.68L0 128h448V55.36ZM448 160H0v288c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64ZM128 416H64v-64h64v64Zm0-96H64v-64h64v64Zm320 96H192v-64h256v64Zm0-96H192v-64h256v64Z'/%3e%3c/svg%3e");
$bg-icon-plot-scatter: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cpath d='M96 0C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96ZM64 176a48 48 0 1 1 48 48 48 48 0 0 1-48-48Zm80 240a48 48 0 1 1 48-48 48 48 0 0 1-48 48Zm128-96a48 48 0 1 1 48-48 48 48 0 0 1-48 48Zm0-160a48 48 0 1 1 48-48 48 48 0 0 1-48 48Zm128 256a48 48 0 1 1 48-48 48 48 0 0 1-48 48Z' data-name='Layer 1'/%3e%3c/g%3e%3c/svg%3e");
$bg-icon-notebook-shift-log: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M448 55.36c0-39.95-27.69-63.66-61.54-52.68L0 128h448V55.36ZM448 160H0v288c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64ZM128 416H64v-64h64v64Zm0-96H64v-64h64v64Zm320 96H192v-64h256v64Zm0-96H192v-64h256v64Z'/%3e%3c/svg%3e");

View File

@ -241,12 +241,10 @@
.bg-icon-tabular { @include glyphBg($bg-icon-tabular); }
.bg-icon-tabular-lad { @include glyphBg($bg-icon-tabular-lad); }
.bg-icon-tabular-lad-set { @include glyphBg($bg-icon-tabular-lad-set); }
.bg-icon-tabular-realtime { @include glyphBg($bg-icon-tabular-realtime); }
.bg-icon-tabular-scrolling { @include glyphBg($bg-icon-tabular-scrolling); }
.bg-icon-telemetry { @include glyphBg($bg-icon-telemetry); }
.bg-icon-timeline { @include glyphBg($bg-icon-timeline); }
.bg-icon-timer { @include glyphBg($bg-icon-timer); }
.bg-icon-topic { @include glyphBg($bg-icon-topic); }
.bg-icon-box-with-dashed-lines { @include glyphBg($bg-icon-box-with-dashed-lines); }
.bg-icon-summary-widget { @include glyphBg($bg-icon-summary-widget); }
.bg-icon-notebook { @include glyphBg($bg-icon-notebook); }
@ -264,5 +262,5 @@
.bg-icon-map { @include glyphBg($bg-icon-map); }
.bg-icon-plan { @include glyphBg($bg-icon-plan); }
.bg-icon-timelist { @include glyphBg($bg-icon-timelist); }
.bg-icon-notebook-shift-log { @include glyphBg($bg-icon-notebook-shift-log); }
.bg-icon-plot-scatter { @include glyphBg($bg-icon-plot-scatter); }
.bg-icon-notebook-shift-log { @include glyphBg($bg-icon-notebook-shift-log); }

View File

@ -5,6 +5,7 @@
>
<input
class="c-search__input"
aria-label="Search Input"
tabindex="10000"
type="search"
v-bind="$attrs"

View File

@ -100,7 +100,7 @@ export default {
}
},
async mounted() {
this.annontation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
this.annotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
this.addAnnotationListener(this.annotation);
if (this.annotation && this.annotation.tags) {
this.tagsChanged(this.annotation.tags);
@ -133,21 +133,18 @@ export default {
this.addedTags.push(newTagValue);
this.userAddingTag = true;
},
async tagRemoved(tagToRemove) {
const existingAnnotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
return this.openmct.annotation.removeAnnotationTag(existingAnnotation, tagToRemove);
tagRemoved(tagToRemove) {
return this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
},
async tagAdded(newTag) {
const existingAnnotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
const newAnnotation = await this.openmct.annotation.addAnnotationTag(existingAnnotation,
const annotationWasCreated = this.annotation === null || this.annotation === undefined;
this.annotation = await this.openmct.annotation.addAnnotationTag(this.annotation,
this.domainObject, this.targetSpecificDetails, this.annotationType, newTag);
if (!this.annotation) {
this.addAnnotationListener(newAnnotation);
if (annotationWasCreated) {
this.addAnnotationListener(this.annotation);
}
this.tagsChanged(newAnnotation.tags);
this.tagsChanged(this.annotation.tags);
this.userAddingTag = false;
}
}

View File

@ -23,6 +23,8 @@
<template>
<div
class="c-gsearch-result c-gsearch-result--annotation"
aria-label="Search Result"
role="presentation"
>
<div
class="c-gsearch-result__type-icon"

View File

@ -64,8 +64,6 @@ export default {
objectSearchResults: []
};
},
mounted() {
},
destroyed() {
document.body.removeEventListener('click', this.handleOutsideClick);
},
@ -135,8 +133,11 @@ export default {
// dropdown is visible, this will collapse the dropdown.
if (this.$refs.GrandSearch) {
const clickedInsideDropdown = this.$refs.GrandSearch.contains(event.target);
if (!clickedInsideDropdown && this.$refs.searchResultsDropDown._data.resultsShown) {
this.$refs.searchResultsDropDown._data.resultsShown = false;
const clickedPreviewClose = event.target.parentElement && event.target.parentElement.querySelector('.js-preview-window');
const searchResultsDropDown = this.$refs.searchResultsDropDown._data;
if (!clickedInsideDropdown && searchResultsDropDown.resultsShown && !searchResultsDropDown.previewVisible && !clickedPreviewClose) {
searchResultsDropDown.resultsShown = false;
document.body.removeEventListener('click', this.handleOutsideClick);
}
}
}

View File

@ -0,0 +1,203 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 {
createOpenMct,
resetApplicationState
} from 'utils/testing';
import Vue from 'vue';
import GrandSearch from './GrandSearch.vue';
import ExampleTagsPlugin from '../../../../example/exampleTags/plugin';
import DisplayLayoutPlugin from '../../../plugins/displayLayout/plugin';
describe("GrandSearch", () => {
let openmct;
let grandSearchComponent;
let viewContainer;
let parent;
let sharedWorkerToRestore;
let mockDomainObject;
let mockAnnotationObject;
let mockDisplayLayout;
let mockFolderObject;
let originalRouterPath;
beforeEach((done) => {
openmct = createOpenMct();
originalRouterPath = openmct.router.path;
openmct.router.path = [mockDisplayLayout];
openmct.editor.edit();
openmct.install(new ExampleTagsPlugin());
openmct.install(new DisplayLayoutPlugin());
const availableTags = openmct.annotation.getAvailableTags();
mockDomainObject = {
type: 'notebook',
name: 'fooRabbitNotebook',
identifier: {
key: 'some-object',
namespace: 'fooNameSpace'
},
configuration: {
entries: {
someSection: {
somePage: [
{
id: 'fooBarEntry',
text: 'Foo Bar Text'
}
]
}
}
}
};
mockFolderObject = {
type: 'folder',
name: 'Test Folder',
identifier: {
key: 'some-folder',
namespace: 'fooNameSpace'
}
};
mockDisplayLayout = {
type: 'layout',
name: 'Bar Layout',
identifier: {
key: 'some-layout',
namespace: 'fooNameSpace'
},
configuration: {
items: [],
layoutGrid: [10, 10]
}
};
mockAnnotationObject = {
type: 'annotation',
name: 'Some Notebook Annotation',
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: [availableTags[0].id, availableTags[1].id],
identifier: {
key: 'anAnnotationKey',
namespace: 'fooNameSpace'
},
targets: {
'fooNameSpace:some-object': {
entryId: 'fooBarEntry'
}
}
};
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);
const mockObjectProvider = jasmine.createSpyObj("mock object provider", [
"create",
"update",
"get"
]);
// eslint-disable-next-line require-await
mockObjectProvider.get = async (identifier) => {
if (identifier.key === mockDomainObject.identifier.key) {
return mockDomainObject;
} else if (identifier.key === mockAnnotationObject.identifier.key) {
return mockAnnotationObject;
} else if (identifier.key === mockDisplayLayout.identifier.key) {
return mockDisplayLayout;
} else if (identifier.key === mockFolderObject.identifier.key) {
return mockFolderObject;
} else {
return null;
}
};
mockObjectProvider.create.and.returnValue(Promise.resolve(true));
mockObjectProvider.update.and.returnValue(Promise.resolve(true));
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
const mockViewProvider = jasmine.createSpyObj("mock view provider", [
"key",
"view",
"canView"
]);
openmct.objectViews.addProvider(mockViewProvider);
openmct.on('start', async () => {
// use local worker
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null;
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
await openmct.objects.inMemorySearchProvider.index(mockDisplayLayout);
await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
parent = document.createElement('div');
document.body.appendChild(parent);
viewContainer = document.createElement('div');
parent.append(viewContainer);
grandSearchComponent = new Vue({
el: viewContainer,
components: {
GrandSearch
},
provide: {
openmct
},
template: '<GrandSearch/>'
}).$mount();
await Vue.nextTick();
done();
});
openmct.startHeadless();
});
afterEach(() => {
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
openmct.router.path = originalRouterPath;
grandSearchComponent.$destroy();
return resetApplicationState(openmct);
});
it("should render an object search result", async () => {
await grandSearchComponent.$children[0].searchEverything('foo');
await Vue.nextTick();
const searchResult = document.querySelector('[aria-label="fooRabbitNotebook notebook result"]');
expect(searchResult).toBeDefined();
});
it("should render an annotation search result", async () => {
await grandSearchComponent.$children[0].searchEverything('S');
await Vue.nextTick();
const annotationResult = document.querySelector('[aria-label="Search Result"]');
expect(annotationResult).toBeDefined();
});
it("should preview object search results in edit mode if object clicked", async () => {
await grandSearchComponent.$children[0].searchEverything('Folder');
grandSearchComponent._provided.openmct.router.path = [mockDisplayLayout];
await Vue.nextTick();
const searchResult = document.querySelector('[name="Test Folder"]');
expect(searchResult).toBeDefined();
searchResult.click();
const previewWindow = document.querySelector('.js-preview-window');
expect(previewWindow).toBeDefined();
});
});

View File

@ -23,6 +23,7 @@
<template>
<div
class="c-gsearch-result c-gsearch-result--object"
aria-label="Search Result"
role="presentation"
>
<div
@ -37,6 +38,8 @@
<div
class="c-gsearch-result__title"
:name="resultName"
draggable="true"
@dragstart="dragStart"
@click="clickedResult"
>
{{ resultName }}
@ -56,6 +59,7 @@
<script>
import ObjectPath from '../../components/ObjectPath.vue';
import objectPathToUrl from '../../../tools/url';
import PreviewAction from '../../preview/PreviewAction';
export default {
name: 'ObjectSearchResult',
@ -90,12 +94,43 @@ export default {
}
};
this.$refs.objectpath.updateSelection([[selectionObject]]);
this.previewAction = new PreviewAction(this.openmct);
this.previewAction.on('isVisible', this.togglePreviewState);
},
destroyed() {
this.previewAction.off('isVisible', this.togglePreviewState);
},
methods: {
clickedResult() {
clickedResult(event) {
if (this.openmct.editor.isEditing()) {
event.preventDefault();
this.preview();
} else {
const objectPath = this.result.originalPath;
const resultUrl = objectPathToUrl(this.openmct, objectPath);
this.openmct.router.navigate(resultUrl);
}
},
togglePreviewState(previewState) {
this.$emit('preview-changed', previewState);
},
preview() {
const objectPath = this.result.originalPath;
const resultUrl = objectPathToUrl(this.openmct, objectPath);
this.openmct.router.navigate(resultUrl);
if (this.previewAction.appliesTo(objectPath)) {
this.previewAction.invoke(objectPath);
}
},
dragStart(event) {
const navigatedObject = this.openmct.router.path[0];
const objectPath = this.result.originalPath;
const serializedPath = JSON.stringify(objectPath);
const keyString = this.openmct.objects.makeKeyString(this.result.identifier);
if (this.openmct.composition.checkPolicy(navigatedObject, this.result)) {
event.dataTransfer.setData("openmct/composable-domain-object", JSON.stringify(this.result));
}
event.dataTransfer.setData("openmct/domain-object-path", serializedPath);
event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.result);
}
}
};

View File

@ -42,6 +42,7 @@
v-for="(objectResult, index) in objectResults"
:key="index"
:result="objectResult"
@preview-changed="previewChanged"
@click.native="selectedResult"
/>
</div>
@ -76,12 +77,18 @@ export default {
return {
resultsShown: false,
annotationResults: [],
objectResults: []
objectResults: [],
previewVisible: false
};
},
methods: {
selectedResult() {
this.resultsShown = false;
if (!this.previewVisible) {
this.resultsShown = false;
}
},
previewChanged(changedPreviewState) {
this.previewVisible = changedPreviewState;
},
showResults(passedAnnotationResults, passedObjectResults) {
if ((passedAnnotationResults && passedAnnotationResults.length)

View File

@ -39,6 +39,7 @@
padding: $interiorMarginLg;
min-width: 500px;
max-height: 500px;
z-index: 60;
}
&__results,

View File

@ -21,9 +21,11 @@
*****************************************************************************/
import Preview from './Preview.vue';
import Vue from 'vue';
import EventEmitter from 'EventEmitter';
export default class PreviewAction {
export default class PreviewAction extends EventEmitter {
constructor(openmct) {
super();
/**
* Metadata
*/
@ -75,10 +77,12 @@ export default class PreviewAction {
onDestroy: () => {
PreviewAction.isVisible = false;
preview.$destroy();
this.emit('isVisible', false);
}
});
PreviewAction.isVisible = true;
this.emit('isVisible', true);
}
appliesTo(objectPath, view = {}) {

View File

@ -38,6 +38,7 @@ export default class DefaultClock extends EventEmitter {
this.cssClass = 'icon-clock';
this.name = 'Clock';
this.description = "A default clock for openmct.";
this.initialized = false;
}
tick(tickValue) {
@ -86,4 +87,7 @@ export default class DefaultClock extends EventEmitter {
return this.lastTick;
}
isInitialized() {
return this.initialized;
}
}