mirror of
https://github.com/nasa/openmct.git
synced 2025-06-17 06:38:17 +00:00
* feat(IndicatorAPI): accept Vue components - Adds a new property to Indicators, `component`, which is a synchronous or asynchronous Vue component. - Adds `wrapHtmlElement` utility function to create anonymous Vue components out of `HTMLElement`s (for backwards compatibility) - Refactors StatusIndicators.vue to use dynamic components, allowing us to dynamically render indicators (and keep it all within Vue's ecosystem). * refactor(indicators): use dynamic Vue components instead of `mount()` - Refactors some indicators to use Vue components directly as async components * refactor: use Vue reactivity for timestamps in clock indicator * fix(test): fix unit tests and remove some console logs * test(e2e): stabilize ladSet e2e test * test: mix in some Vue indicators in indicatorSpec * refactor: cleanup variable names * docs: update IndicatorAPI docs * fix(e2e): wait for async status bar components to load before snapshot * a11y(e2e): add aria-labels and wait for status bar to load * test(e2e): add exact: true * fix: initializing indicators * fix(typo): uhhh.. how did that get there? O_o * fix: use synchronous components for default indicators * test: clean up, remove unnecessary `nextTick()`s * test: remove more `nextTick()`s * refactor: lint:fix * fix: `on` -> `off` * test(e2e): stabilize tabs test * test(e2e): attempt to stabilize limit lines tests with `toHaveCount()` assertion
This commit is contained in:
@ -260,9 +260,9 @@ async function assertLimitLinesExistAndAreVisible(page) {
|
|||||||
await waitForPlotsToRender(page);
|
await waitForPlotsToRender(page);
|
||||||
// Wait for limit lines to be created
|
// Wait for limit lines to be created
|
||||||
await page.waitForSelector('.js-limit-area', { state: 'attached' });
|
await page.waitForSelector('.js-limit-area', { state: 'attached' });
|
||||||
const limitLineCount = await page.locator('.c-plot-limit-line').count();
|
|
||||||
// There should be 10 limit lines created by default
|
// There should be 10 limit lines created by default
|
||||||
expect(await page.locator('.c-plot-limit-line').count()).toBe(10);
|
await expect(page.locator('.c-plot-limit-line')).toHaveCount(10);
|
||||||
|
const limitLineCount = await page.locator('.c-plot-limit-line').count();
|
||||||
for (let i = 0; i < limitLineCount; i++) {
|
for (let i = 0; i < limitLineCount; i++) {
|
||||||
await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible();
|
await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible();
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ test.describe('Tabs View', () => {
|
|||||||
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||||
|
|
||||||
// no canvas (i.e., sine wave generator) in the document should be visible
|
// no canvas (i.e., sine wave generator) in the document should be visible
|
||||||
await expect(page.locator('canvas')).toBeHidden();
|
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
||||||
|
|
||||||
// select second tab
|
// select second tab
|
||||||
await page.getByLabel(`${notebook.name} tab`).click();
|
await page.getByLabel(`${notebook.name} tab`).click();
|
||||||
@ -64,7 +64,7 @@ test.describe('Tabs View', () => {
|
|||||||
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
||||||
|
|
||||||
// no canvas (i.e., sine wave generator) in the document should be visible
|
// no canvas (i.e., sine wave generator) in the document should be visible
|
||||||
await expect(page.locator('canvas')).toBeHidden();
|
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
||||||
|
|
||||||
// select third tab
|
// select third tab
|
||||||
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
|
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
|
||||||
@ -83,6 +83,6 @@ test.describe('Tabs View', () => {
|
|||||||
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||||
|
|
||||||
// no canvas (i.e., sine wave generator) in the document should be visible
|
// no canvas (i.e., sine wave generator) in the document should be visible
|
||||||
await expect(page.locator('canvas')).toBeHidden();
|
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -99,7 +99,7 @@ test.describe('Grand Search', () => {
|
|||||||
page.waitForNavigation(),
|
page.waitForNavigation(),
|
||||||
page.getByLabel('OpenMCT Search').getByText('Clock A').click()
|
page.getByLabel('OpenMCT Search').getByText('Clock A').click()
|
||||||
]);
|
]);
|
||||||
await expect(page.getByRole('status', { name: 'Clock' })).toBeVisible();
|
await expect(page.getByRole('status', { name: 'Clock', exact: true })).toBeVisible();
|
||||||
|
|
||||||
await grandSearchInput.fill('Disp');
|
await grandSearchInput.fill('Disp');
|
||||||
await expect(page.getByLabel('Object Search Result').first()).toContainText(
|
await expect(page.getByLabel('Object Search Result').first()).toContainText(
|
||||||
|
@ -36,6 +36,22 @@ test.describe('Visual - Header @a11y', () => {
|
|||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
//Go to baseURL and Hide Tree
|
//Go to baseURL and Hide Tree
|
||||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||||
|
// Wait for status bar to load
|
||||||
|
await expect(
|
||||||
|
page.getByRole('status', {
|
||||||
|
name: 'Clock Indicator'
|
||||||
|
})
|
||||||
|
).toBeInViewport();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('status', {
|
||||||
|
name: 'Global Clear Indicator'
|
||||||
|
})
|
||||||
|
).toBeInViewport();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('status', {
|
||||||
|
name: 'Snapshot Indicator'
|
||||||
|
})
|
||||||
|
).toBeInViewport();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('header sizing', async ({ page, theme }) => {
|
test('header sizing', async ({ page, theme }) => {
|
||||||
|
@ -23,7 +23,7 @@ import percySnapshot from '@percy/playwright';
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
import * as utils from '../../helper/faultUtils.js';
|
import * as utils from '../../helper/faultUtils.js';
|
||||||
import { test } from '../../pluginFixtures.js';
|
import { expect, test } from '../../pluginFixtures.js';
|
||||||
|
|
||||||
test.describe('Fault Management Visual Tests', () => {
|
test.describe('Fault Management Visual Tests', () => {
|
||||||
test('icon test', async ({ page, theme }) => {
|
test('icon test', async ({ page, theme }) => {
|
||||||
@ -32,6 +32,23 @@ test.describe('Fault Management Visual Tests', () => {
|
|||||||
});
|
});
|
||||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
// Wait for status bar to load
|
||||||
|
await expect(
|
||||||
|
page.getByRole('status', {
|
||||||
|
name: 'Clock Indicator'
|
||||||
|
})
|
||||||
|
).toBeInViewport();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('status', {
|
||||||
|
name: 'Global Clear Indicator'
|
||||||
|
})
|
||||||
|
).toBeInViewport();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('status', {
|
||||||
|
name: 'Snapshot Indicator'
|
||||||
|
})
|
||||||
|
).toBeInViewport();
|
||||||
|
|
||||||
await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`);
|
await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -22,9 +22,12 @@
|
|||||||
|
|
||||||
import EventEmitter from 'EventEmitter';
|
import EventEmitter from 'EventEmitter';
|
||||||
|
|
||||||
|
import vueWrapHtmlElement from '../../utils/vueWrapHtmlElement.js';
|
||||||
import SimpleIndicator from './SimpleIndicator.js';
|
import SimpleIndicator from './SimpleIndicator.js';
|
||||||
|
|
||||||
class IndicatorAPI extends EventEmitter {
|
class IndicatorAPI extends EventEmitter {
|
||||||
|
/** @type {import('../../../openmct.js').OpenMCT} */
|
||||||
|
openmct;
|
||||||
constructor(openmct) {
|
constructor(openmct) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@ -42,6 +45,18 @@ class IndicatorAPI extends EventEmitter {
|
|||||||
return new SimpleIndicator(this.openmct);
|
return new SimpleIndicator(this.openmct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('vue').Component} VueComponent
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Indicator
|
||||||
|
* @property {HTMLElement} [element]
|
||||||
|
* @property {VueComponent|Promise<VueComponent>} [vueComponent]
|
||||||
|
* @property {string} key
|
||||||
|
* @property {number} priority
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accepts an indicator object, which is a simple object
|
* Accepts an indicator object, which is a simple object
|
||||||
* with a two attributes: 'element' which has an HTMLElement
|
* with a two attributes: 'element' which has an HTMLElement
|
||||||
@ -62,11 +77,20 @@ class IndicatorAPI extends EventEmitter {
|
|||||||
* myIndicator.text("Hello World!");
|
* myIndicator.text("Hello World!");
|
||||||
* myIndicator.iconClass("icon-info");
|
* myIndicator.iconClass("icon-info");
|
||||||
*
|
*
|
||||||
|
* If you would like to use a Vue component, you can pass it in
|
||||||
|
* directly as the 'vueComponent' attribute of the indicator object.
|
||||||
|
* This accepts a Vue component or a promise that resolves to a Vue component (for asynchronous
|
||||||
|
* rendering).
|
||||||
|
*
|
||||||
|
* @param {Indicator} indicator
|
||||||
*/
|
*/
|
||||||
add(indicator) {
|
add(indicator) {
|
||||||
if (!indicator.priority) {
|
if (!indicator.priority) {
|
||||||
indicator.priority = this.openmct.priority.DEFAULT;
|
indicator.priority = this.openmct.priority.DEFAULT;
|
||||||
}
|
}
|
||||||
|
if (!indicator.vueComponent) {
|
||||||
|
indicator.vueComponent = vueWrapHtmlElement(indicator.element);
|
||||||
|
}
|
||||||
|
|
||||||
this.indicatorObjects.push(indicator);
|
this.indicatorObjects.push(indicator);
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
import { createOpenMct, resetApplicationState } from '../../utils/testing.js';
|
import { createOpenMct, resetApplicationState } from '../../utils/testing.js';
|
||||||
import SimpleIndicator from './SimpleIndicator.js';
|
import SimpleIndicator from './SimpleIndicator.js';
|
||||||
|
|
||||||
@ -33,7 +35,7 @@ describe('The Indicator API', () => {
|
|||||||
return resetApplicationState(openmct);
|
return resetApplicationState(openmct);
|
||||||
});
|
});
|
||||||
|
|
||||||
function generateIndicator(className, label, priority) {
|
function generateHTMLIndicator(className, label, priority) {
|
||||||
const element = document.createElement('div');
|
const element = document.createElement('div');
|
||||||
element.classList.add(className);
|
element.classList.add(className);
|
||||||
const textNode = document.createTextNode(label);
|
const textNode = document.createTextNode(label);
|
||||||
@ -46,8 +48,25 @@ describe('The Indicator API', () => {
|
|||||||
return testIndicator;
|
return testIndicator;
|
||||||
}
|
}
|
||||||
|
|
||||||
it('can register an indicator', () => {
|
function generateVueIndicator(priority) {
|
||||||
const testIndicator = generateIndicator('test-indicator', 'This is a test indicator', 2);
|
return {
|
||||||
|
vueComponent: defineComponent({
|
||||||
|
template: '<div class="test-indicator">This is a test indicator</div>'
|
||||||
|
}),
|
||||||
|
priority
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it('can register an HTML indicator', () => {
|
||||||
|
const testIndicator = generateHTMLIndicator('test-indicator', 'This is a test indicator', 2);
|
||||||
|
openmct.indicators.add(testIndicator);
|
||||||
|
expect(openmct.indicators.indicatorObjects).toBeDefined();
|
||||||
|
// notifier indicator is installed by default
|
||||||
|
expect(openmct.indicators.indicatorObjects.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can register a Vue indicator', () => {
|
||||||
|
const testIndicator = generateVueIndicator(2);
|
||||||
openmct.indicators.add(testIndicator);
|
openmct.indicators.add(testIndicator);
|
||||||
expect(openmct.indicators.indicatorObjects).toBeDefined();
|
expect(openmct.indicators.indicatorObjects).toBeDefined();
|
||||||
// notifier indicator is installed by default
|
// notifier indicator is installed by default
|
||||||
@ -55,37 +74,40 @@ describe('The Indicator API', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can order indicators based on priority', () => {
|
it('can order indicators based on priority', () => {
|
||||||
const testIndicator1 = generateIndicator(
|
const testIndicator1 = generateHTMLIndicator(
|
||||||
'test-indicator-1',
|
'test-indicator-1',
|
||||||
'This is a test indicator',
|
'This is a test indicator',
|
||||||
openmct.priority.LOW
|
openmct.priority.LOW
|
||||||
);
|
);
|
||||||
openmct.indicators.add(testIndicator1);
|
openmct.indicators.add(testIndicator1);
|
||||||
|
|
||||||
const testIndicator2 = generateIndicator(
|
const testIndicator2 = generateHTMLIndicator(
|
||||||
'test-indicator-2',
|
'test-indicator-2',
|
||||||
'This is another test indicator',
|
'This is another test indicator',
|
||||||
openmct.priority.DEFAULT
|
openmct.priority.DEFAULT
|
||||||
);
|
);
|
||||||
openmct.indicators.add(testIndicator2);
|
openmct.indicators.add(testIndicator2);
|
||||||
|
|
||||||
const testIndicator3 = generateIndicator(
|
const testIndicator3 = generateHTMLIndicator(
|
||||||
'test-indicator-3',
|
'test-indicator-3',
|
||||||
'This is yet another test indicator',
|
'This is yet another test indicator',
|
||||||
openmct.priority.LOW
|
openmct.priority.LOW
|
||||||
);
|
);
|
||||||
openmct.indicators.add(testIndicator3);
|
openmct.indicators.add(testIndicator3);
|
||||||
|
|
||||||
const testIndicator4 = generateIndicator(
|
const testIndicator4 = generateHTMLIndicator(
|
||||||
'test-indicator-4',
|
'test-indicator-4',
|
||||||
'This is yet another test indicator',
|
'This is yet another test indicator',
|
||||||
openmct.priority.HIGH
|
openmct.priority.HIGH
|
||||||
);
|
);
|
||||||
openmct.indicators.add(testIndicator4);
|
openmct.indicators.add(testIndicator4);
|
||||||
|
|
||||||
expect(openmct.indicators.indicatorObjects.length).toBe(5);
|
const testIndicator5 = generateVueIndicator(openmct.priority.DEFAULT);
|
||||||
|
openmct.indicators.add(testIndicator5);
|
||||||
|
|
||||||
|
expect(openmct.indicators.indicatorObjects.length).toBe(6);
|
||||||
const indicatorObjectsByPriority = openmct.indicators.getIndicatorObjectsByPriority();
|
const indicatorObjectsByPriority = openmct.indicators.getIndicatorObjectsByPriority();
|
||||||
expect(indicatorObjectsByPriority.length).toBe(5);
|
expect(indicatorObjectsByPriority.length).toBe(6);
|
||||||
expect(indicatorObjectsByPriority[2].priority).toBe(openmct.priority.DEFAULT);
|
expect(indicatorObjectsByPriority[2].priority).toBe(openmct.priority.DEFAULT);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -20,7 +20,10 @@
|
|||||||
at runtime from the About dialog for additional information.
|
at runtime from the About dialog for additional information.
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="c-indicator c-indicator--clickable icon-clear-data s-status-caution">
|
<div
|
||||||
|
aria-label="Global Clear Indicator"
|
||||||
|
class="c-indicator c-indicator--clickable icon-clear-data s-status-caution"
|
||||||
|
>
|
||||||
<span class="label c-indicator__label">
|
<span class="label c-indicator__label">
|
||||||
<button @click="globalClearEmit">Clear Data</button>
|
<button @click="globalClearEmit">Clear Data</button>
|
||||||
</span>
|
</span>
|
||||||
|
@ -19,8 +19,6 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
import mount from 'utils/mount';
|
|
||||||
|
|
||||||
import ClearDataAction from './ClearDataAction.js';
|
import ClearDataAction from './ClearDataAction.js';
|
||||||
import GlobalClearIndicator from './components/GlobalClearIndicator.vue';
|
import GlobalClearIndicator from './components/GlobalClearIndicator.vue';
|
||||||
|
|
||||||
@ -31,27 +29,10 @@ export default function plugin(appliesToObjects, options = { indicator: true })
|
|||||||
|
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
if (installIndicator) {
|
if (installIndicator) {
|
||||||
const { vNode, destroy } = mount(
|
|
||||||
{
|
|
||||||
components: {
|
|
||||||
GlobalClearIndicator
|
|
||||||
},
|
|
||||||
provide: {
|
|
||||||
openmct
|
|
||||||
},
|
|
||||||
template: '<GlobalClearIndicator></GlobalClearIndicator>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
app: openmct.app,
|
|
||||||
element: document.createElement('div')
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let indicator = {
|
let indicator = {
|
||||||
element: vNode.el,
|
vueComponent: GlobalClearIndicator,
|
||||||
key: 'global-clear-indicator',
|
key: 'global-clear-indicator',
|
||||||
priority: openmct.priority.DEFAULT,
|
priority: openmct.priority.DEFAULT
|
||||||
destroy: destroy
|
|
||||||
};
|
};
|
||||||
|
|
||||||
openmct.indicators.add(indicator);
|
openmct.indicators.add(indicator);
|
||||||
|
@ -21,7 +21,6 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/testing';
|
import { createMouseEvent, createOpenMct, resetApplicationState } from 'utils/testing';
|
||||||
import { nextTick } from 'vue';
|
|
||||||
|
|
||||||
import ClearDataPlugin from './plugin.js';
|
import ClearDataPlugin from './plugin.js';
|
||||||
|
|
||||||
@ -208,12 +207,11 @@ describe('The Clear Data Plugin:', () => {
|
|||||||
it('installs', () => {
|
it('installs', () => {
|
||||||
const globalClearIndicator = openmct.indicators.indicatorObjects.find(
|
const globalClearIndicator = openmct.indicators.indicatorObjects.find(
|
||||||
(indicator) => indicator.key === 'global-clear-indicator'
|
(indicator) => indicator.key === 'global-clear-indicator'
|
||||||
).element;
|
).vueComponent;
|
||||||
expect(globalClearIndicator).toBeDefined();
|
expect(globalClearIndicator).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders its major elements', async () => {
|
it('renders its major elements', () => {
|
||||||
await nextTick();
|
|
||||||
const indicatorClass = appHolder.querySelector('.c-indicator');
|
const indicatorClass = appHolder.querySelector('.c-indicator');
|
||||||
const iconClass = appHolder.querySelector('.icon-clear-data');
|
const iconClass = appHolder.querySelector('.icon-clear-data');
|
||||||
const indicatorLabel = appHolder.querySelector('.c-indicator__label');
|
const indicatorLabel = appHolder.querySelector('.c-indicator__label');
|
||||||
@ -228,10 +226,7 @@ describe('The Clear Data Plugin:', () => {
|
|||||||
const indicatorLabel = appHolder.querySelector('.c-indicator__label');
|
const indicatorLabel = appHolder.querySelector('.c-indicator__label');
|
||||||
const buttonElement = indicatorLabel.querySelector('button');
|
const buttonElement = indicatorLabel.querySelector('button');
|
||||||
const clickEvent = createMouseEvent('click');
|
const clickEvent = createMouseEvent('click');
|
||||||
openmct.objectViews.on('clearData', () => {
|
openmct.objectViews.on('clearData', done);
|
||||||
// when we click the button, this event should fire
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
buttonElement.dispatchEvent(clickEvent);
|
buttonElement.dispatchEvent(clickEvent);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
aria-label="Clock Indicator"
|
||||||
class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable"
|
class="c-indicator t-indicator-clock icon-clock no-minify c-indicator--not-clickable"
|
||||||
role="complementary"
|
role="complementary"
|
||||||
>
|
>
|
||||||
@ -40,27 +41,32 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
indicatorFormat: {
|
indicatorFormat: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
default: 'YYYY/MM/DD HH:mm:ss'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
timeTextValue: this.openmct.time.getClock() ? this.openmct.time.now() : undefined
|
timestamp: this.openmct.time.getClock() ? this.openmct.time.now() : undefined
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
timeTextValue() {
|
||||||
|
return `${moment.utc(this.timestamp).format(this.indicatorFormat)} ${
|
||||||
|
this.openmct.time.getTimeSystem().name
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.tick = raf(this.tick);
|
this.tick = raf(this.tick);
|
||||||
this.openmct.time.on('tick', this.tick);
|
this.openmct.time.on('tick', this.tick);
|
||||||
this.tick(this.timeTextValue);
|
this.tick(this.timestamp);
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.openmct.time.off('tick', this.tick);
|
this.openmct.time.off('tick', this.tick);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
tick(timestamp) {
|
tick(timestamp) {
|
||||||
this.timeTextValue = `${moment.utc(timestamp).format(this.indicatorFormat)} ${
|
this.timestamp = timestamp;
|
||||||
this.openmct.time.getTimeSystem().name
|
|
||||||
}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -21,14 +21,12 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import momentTimezone from 'moment-timezone';
|
import momentTimezone from 'moment-timezone';
|
||||||
import mount from 'utils/mount';
|
|
||||||
|
|
||||||
import ClockViewProvider from './ClockViewProvider.js';
|
import ClockViewProvider from './ClockViewProvider.js';
|
||||||
import ClockIndicator from './components/ClockIndicator.vue';
|
import ClockIndicator from './components/ClockIndicator.vue';
|
||||||
|
|
||||||
export default function ClockPlugin(options) {
|
export default function ClockPlugin(options) {
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
const CLOCK_INDICATOR_FORMAT = 'YYYY/MM/DD HH:mm:ss';
|
|
||||||
openmct.types.addType('clock', {
|
openmct.types.addType('clock', {
|
||||||
name: 'Clock',
|
name: 'Clock',
|
||||||
description:
|
description:
|
||||||
@ -92,31 +90,9 @@ export default function ClockPlugin(options) {
|
|||||||
});
|
});
|
||||||
openmct.objectViews.addProvider(new ClockViewProvider(openmct));
|
openmct.objectViews.addProvider(new ClockViewProvider(openmct));
|
||||||
|
|
||||||
if (options && options.enableClockIndicator === true) {
|
if (options?.enableClockIndicator === true) {
|
||||||
const element = document.createElement('div');
|
|
||||||
|
|
||||||
const { vNode } = mount(
|
|
||||||
{
|
|
||||||
components: {
|
|
||||||
ClockIndicator
|
|
||||||
},
|
|
||||||
provide: {
|
|
||||||
openmct
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
indicatorFormat: CLOCK_INDICATOR_FORMAT
|
|
||||||
};
|
|
||||||
},
|
|
||||||
template: '<ClockIndicator :indicator-format="indicatorFormat" />'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
app: openmct.app,
|
|
||||||
element
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const indicator = {
|
const indicator = {
|
||||||
element: vNode.el,
|
vueComponent: ClockIndicator,
|
||||||
key: 'clock-indicator',
|
key: 'clock-indicator',
|
||||||
priority: openmct.priority.LOW
|
priority: openmct.priority.LOW
|
||||||
};
|
};
|
||||||
|
@ -195,10 +195,6 @@ describe('Clock plugin:', () => {
|
|||||||
let clockIndicator;
|
let clockIndicator;
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
if (clockIndicator) {
|
|
||||||
clockIndicator.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
clockIndicator = undefined;
|
clockIndicator = undefined;
|
||||||
if (appHolder) {
|
if (appHolder) {
|
||||||
appHolder.remove();
|
appHolder.remove();
|
||||||
@ -223,7 +219,7 @@ describe('Clock plugin:', () => {
|
|||||||
|
|
||||||
clockIndicator = openmct.indicators.indicatorObjects.find(
|
clockIndicator = openmct.indicators.indicatorObjects.find(
|
||||||
(indicator) => indicator.key === 'clock-indicator'
|
(indicator) => indicator.key === 'clock-indicator'
|
||||||
).element;
|
).vueComponent;
|
||||||
|
|
||||||
const hasClockIndicator = clockIndicator !== null && clockIndicator !== undefined;
|
const hasClockIndicator = clockIndicator !== null && clockIndicator !== undefined;
|
||||||
expect(hasClockIndicator).toBe(true);
|
expect(hasClockIndicator).toBe(true);
|
||||||
@ -231,14 +227,16 @@ describe('Clock plugin:', () => {
|
|||||||
|
|
||||||
it('contains text', async () => {
|
it('contains text', async () => {
|
||||||
await setupClock(true);
|
await setupClock(true);
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||||
|
|
||||||
clockIndicator = openmct.indicators.indicatorObjects.find(
|
clockIndicator = openmct.indicators.indicatorObjects.find(
|
||||||
(indicator) => indicator.key === 'clock-indicator'
|
(indicator) => indicator.key === 'clock-indicator'
|
||||||
).element;
|
).vueComponent;
|
||||||
const clockIndicatorText = clockIndicator.textContent.trim();
|
const hasClockIndicator = clockIndicator !== null && clockIndicator !== undefined;
|
||||||
|
expect(hasClockIndicator).toBe(true);
|
||||||
|
const clockIndicatorText = appHolder
|
||||||
|
.querySelector('.t-indicator-clock .c-indicator__label')
|
||||||
|
.textContent.trim();
|
||||||
const textIncludesUTC = clockIndicatorText.includes('UTC');
|
const textIncludesUTC = clockIndicatorText.includes('UTC');
|
||||||
|
|
||||||
expect(textIncludesUTC).toBe(true);
|
expect(textIncludesUTC).toBe(true);
|
||||||
|
@ -106,7 +106,6 @@ describe('the plugin', function () {
|
|||||||
flexibleView.show(child, false);
|
flexibleView.show(child, false);
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
console.log(child);
|
|
||||||
const flexTitle = child.querySelector('.c-fl');
|
const flexTitle = child.querySelector('.c-fl');
|
||||||
|
|
||||||
expect(flexTitle).not.toBeNull();
|
expect(flexTitle).not.toBeNull();
|
||||||
|
@ -48,11 +48,12 @@
|
|||||||
import mount from 'utils/mount';
|
import mount from 'utils/mount';
|
||||||
|
|
||||||
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants.js';
|
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants.js';
|
||||||
|
import { getSnapshotContainer } from '../plugin.js';
|
||||||
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container.js';
|
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container.js';
|
||||||
import SnapshotContainerComponent from './NotebookSnapshotContainer.vue';
|
import SnapshotContainerComponent from './NotebookSnapshotContainer.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['openmct', 'snapshotContainer'],
|
inject: ['openmct'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
expanded: false,
|
expanded: false,
|
||||||
@ -62,6 +63,9 @@ export default {
|
|||||||
flashIndicator: false
|
flashIndicator: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.snapshotContainer = getSnapshotContainer(this.openmct);
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
|
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
|
||||||
this.updateSnapshotIndicatorTitle();
|
this.updateSnapshotIndicatorTitle();
|
||||||
|
@ -20,8 +20,6 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import mount from 'utils/mount';
|
|
||||||
|
|
||||||
import { notebookImageMigration } from '../notebook/utils/notebook-migration.js';
|
import { notebookImageMigration } from '../notebook/utils/notebook-migration.js';
|
||||||
import CopyToNotebookAction from './actions/CopyToNotebookAction.js';
|
import CopyToNotebookAction from './actions/CopyToNotebookAction.js';
|
||||||
import ExportNotebookAsTextAction from './actions/ExportNotebookAsTextAction.js';
|
import ExportNotebookAsTextAction from './actions/ExportNotebookAsTextAction.js';
|
||||||
@ -39,7 +37,7 @@ import NotebookViewProvider from './NotebookViewProvider.js';
|
|||||||
import SnapshotContainer from './snapshot-container.js';
|
import SnapshotContainer from './snapshot-container.js';
|
||||||
|
|
||||||
let notebookSnapshotContainer;
|
let notebookSnapshotContainer;
|
||||||
function getSnapshotContainer(openmct) {
|
export function getSnapshotContainer(openmct) {
|
||||||
if (!notebookSnapshotContainer) {
|
if (!notebookSnapshotContainer) {
|
||||||
notebookSnapshotContainer = new SnapshotContainer(openmct);
|
notebookSnapshotContainer = new SnapshotContainer(openmct);
|
||||||
}
|
}
|
||||||
@ -66,7 +64,6 @@ function installBaseNotebookFunctionality(openmct) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapshotContainer = getSnapshotContainer(openmct);
|
|
||||||
const notebookSnapshotImageType = {
|
const notebookSnapshotImageType = {
|
||||||
name: 'Notebook Snapshot Image Storage',
|
name: 'Notebook Snapshot Image Storage',
|
||||||
description: 'Notebook Snapshot Image Storage object',
|
description: 'Notebook Snapshot Image Storage object',
|
||||||
@ -82,27 +79,10 @@ function installBaseNotebookFunctionality(openmct) {
|
|||||||
openmct.actions.register(new CopyToNotebookAction(openmct));
|
openmct.actions.register(new CopyToNotebookAction(openmct));
|
||||||
openmct.actions.register(new ExportNotebookAsTextAction(openmct));
|
openmct.actions.register(new ExportNotebookAsTextAction(openmct));
|
||||||
|
|
||||||
const { vNode, destroy } = mount(
|
|
||||||
{
|
|
||||||
components: {
|
|
||||||
NotebookSnapshotIndicator
|
|
||||||
},
|
|
||||||
provide: {
|
|
||||||
openmct,
|
|
||||||
snapshotContainer
|
|
||||||
},
|
|
||||||
template: '<NotebookSnapshotIndicator></NotebookSnapshotIndicator>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
app: openmct.app
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const indicator = {
|
const indicator = {
|
||||||
element: vNode.el,
|
vueComponent: NotebookSnapshotIndicator,
|
||||||
key: 'notebook-snapshot-indicator',
|
key: 'notebook-snapshot-indicator',
|
||||||
priority: openmct.priority.DEFAULT,
|
priority: openmct.priority.DEFAULT
|
||||||
destroy: destroy
|
|
||||||
};
|
};
|
||||||
|
|
||||||
openmct.indicators.add(indicator);
|
openmct.indicators.add(indicator);
|
||||||
|
@ -336,32 +336,30 @@ describe('Notebook plugin:', () => {
|
|||||||
let snapshotIndicator;
|
let snapshotIndicator;
|
||||||
let drawerElement;
|
let drawerElement;
|
||||||
|
|
||||||
function clickSnapshotIndicator() {
|
async function clickSnapshotIndicator() {
|
||||||
const indicator = element.querySelector('.icon-camera');
|
const button =
|
||||||
const button = indicator.querySelector('button');
|
appHolder.querySelector('[aria-label="Show Snapshots"]') ??
|
||||||
|
appHolder.querySelector('[aria-label="Hide Snapshots"]');
|
||||||
const clickEvent = createMouseEvent('click');
|
const clickEvent = createMouseEvent('click');
|
||||||
|
|
||||||
button.dispatchEvent(clickEvent);
|
button.dispatchEvent(clickEvent);
|
||||||
|
await nextTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
snapshotIndicator = openmct.indicators.indicatorObjects.find(
|
snapshotIndicator = openmct.indicators.indicatorObjects.find(
|
||||||
(indicator) => indicator.key === 'notebook-snapshot-indicator'
|
(indicator) => indicator.key === 'notebook-snapshot-indicator'
|
||||||
).element;
|
).vueComponent;
|
||||||
|
|
||||||
element.append(snapshotIndicator);
|
await nextTick();
|
||||||
|
|
||||||
return nextTick().then(() => {
|
|
||||||
drawerElement = document.querySelector('.l-shell__drawer');
|
drawerElement = document.querySelector('.l-shell__drawer');
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
if (drawerElement) {
|
if (drawerElement) {
|
||||||
drawerElement.classList.remove('is-expanded');
|
drawerElement.classList.remove('is-expanded');
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshotIndicator.remove();
|
|
||||||
snapshotIndicator = undefined;
|
snapshotIndicator = undefined;
|
||||||
|
|
||||||
if (drawerElement) {
|
if (drawerElement) {
|
||||||
@ -375,11 +373,11 @@ describe('Notebook plugin:', () => {
|
|||||||
expect(hasSnapshotIndicator).toBe(true);
|
expect(hasSnapshotIndicator).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('snapshots container has class isExpanded', () => {
|
it('snapshots container has class isExpanded', async () => {
|
||||||
let classes = drawerElement.classList;
|
let classes = drawerElement.classList;
|
||||||
const isExpandedBefore = classes.contains('is-expanded');
|
const isExpandedBefore = classes.contains('is-expanded');
|
||||||
|
|
||||||
clickSnapshotIndicator();
|
await clickSnapshotIndicator();
|
||||||
classes = drawerElement.classList;
|
classes = drawerElement.classList;
|
||||||
const isExpandedAfterFirstClick = classes.contains('is-expanded');
|
const isExpandedAfterFirstClick = classes.contains('is-expanded');
|
||||||
|
|
||||||
@ -387,15 +385,15 @@ describe('Notebook plugin:', () => {
|
|||||||
expect(isExpandedAfterFirstClick).toBeTrue();
|
expect(isExpandedAfterFirstClick).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('snapshots container does not have class isExpanded', () => {
|
it('snapshots container does not have class isExpanded', async () => {
|
||||||
let classes = drawerElement.classList;
|
let classes = drawerElement.classList;
|
||||||
const isExpandedBefore = classes.contains('is-expanded');
|
const isExpandedBefore = classes.contains('is-expanded');
|
||||||
|
|
||||||
clickSnapshotIndicator();
|
await clickSnapshotIndicator();
|
||||||
classes = drawerElement.classList;
|
classes = drawerElement.classList;
|
||||||
const isExpandedAfterFirstClick = classes.contains('is-expanded');
|
const isExpandedAfterFirstClick = classes.contains('is-expanded');
|
||||||
|
|
||||||
clickSnapshotIndicator();
|
await clickSnapshotIndicator();
|
||||||
classes = drawerElement.classList;
|
classes = drawerElement.classList;
|
||||||
const isExpandedAfterSecondClick = classes.contains('is-expanded');
|
const isExpandedAfterSecondClick = classes.contains('is-expanded');
|
||||||
|
|
||||||
@ -404,8 +402,8 @@ describe('Notebook plugin:', () => {
|
|||||||
expect(isExpandedAfterSecondClick).toBeFalse();
|
expect(isExpandedAfterSecondClick).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('show notebook snapshots container text', () => {
|
it('show notebook snapshots container text', async () => {
|
||||||
clickSnapshotIndicator();
|
await clickSnapshotIndicator();
|
||||||
|
|
||||||
const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name');
|
const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name');
|
||||||
const snapshotsText = notebookSnapshots.textContent.trim();
|
const snapshotsText = notebookSnapshots.textContent.trim();
|
||||||
|
@ -72,8 +72,8 @@ export default {
|
|||||||
this.openmct.notifications.on('dismiss-all', this.updateNotifications);
|
this.openmct.notifications.on('dismiss-all', this.updateNotifications);
|
||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
this.openmct.notifications.of('notification', this.updateNotifications);
|
this.openmct.notifications.off('notification', this.updateNotifications);
|
||||||
this.openmct.notifications.of('dismiss-all', this.updateNotifications);
|
this.openmct.notifications.off('dismiss-all', this.updateNotifications);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
dismissAllNotifications() {
|
dismissAllNotifications() {
|
||||||
|
@ -19,32 +19,14 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
import mount from 'utils/mount';
|
|
||||||
|
|
||||||
import NotificationIndicator from './components/NotificationIndicator.vue';
|
import NotificationIndicator from './components/NotificationIndicator.vue';
|
||||||
|
|
||||||
export default function plugin() {
|
export default function plugin() {
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
const { vNode, destroy } = mount(
|
|
||||||
{
|
|
||||||
components: {
|
|
||||||
NotificationIndicator
|
|
||||||
},
|
|
||||||
provide: {
|
|
||||||
openmct
|
|
||||||
},
|
|
||||||
template: '<NotificationIndicator></NotificationIndicator>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
app: openmct.app
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let indicator = {
|
let indicator = {
|
||||||
key: 'notifications-indicator',
|
key: 'notifications-indicator',
|
||||||
element: vNode.el,
|
vueComponent: NotificationIndicator,
|
||||||
priority: openmct.priority.DEFAULT,
|
priority: openmct.priority.DEFAULT
|
||||||
destroy: destroy
|
|
||||||
};
|
};
|
||||||
openmct.indicators.add(indicator);
|
openmct.indicators.add(indicator);
|
||||||
};
|
};
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
>
|
>
|
||||||
<CreateButton class="l-shell__create-button" />
|
<CreateButton class="l-shell__create-button" />
|
||||||
<GrandSearch ref="grand-search" />
|
<GrandSearch ref="grand-search" />
|
||||||
<StatusIndicators class="l-shell__head-section l-shell__indicators" />
|
<StatusIndicators />
|
||||||
<button
|
<button
|
||||||
class="l-shell__head__collapse-button c-icon-button"
|
class="l-shell__head__collapse-button c-icon-button"
|
||||||
:class="
|
:class="
|
||||||
|
@ -17,24 +17,43 @@
|
|||||||
at runtime from the About dialog for additional information.
|
at runtime from the About dialog for additional information.
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div></div>
|
<div class="l-shell__head-section l-shell__indicators">
|
||||||
|
<component
|
||||||
|
:is="indicator.value.vueComponent"
|
||||||
|
v-for="indicator in sortedIndicators"
|
||||||
|
:key="indicator.value.key"
|
||||||
|
role="status"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { shallowRef } from 'vue';
|
||||||
export default {
|
export default {
|
||||||
inject: ['openmct'],
|
inject: ['openmct'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
indicators: this.openmct.indicators.getIndicatorObjectsByPriority().map(shallowRef)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sortedIndicators() {
|
||||||
|
if (this.indicators.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...this.indicators].sort((a, b) => b.value.priority - a.value.priority);
|
||||||
|
}
|
||||||
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.openmct.indicators.off('addIndicator', this.addIndicator);
|
this.openmct.indicators.off('addIndicator', this.addIndicator);
|
||||||
},
|
},
|
||||||
mounted() {
|
created() {
|
||||||
this.openmct.indicators.getIndicatorObjectsByPriority().forEach(this.addIndicator);
|
|
||||||
|
|
||||||
this.openmct.indicators.on('addIndicator', this.addIndicator);
|
this.openmct.indicators.on('addIndicator', this.addIndicator);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addIndicator(indicator) {
|
addIndicator(indicator) {
|
||||||
this.$el.appendChild(indicator.element);
|
this.indicators.push(shallowRef(indicator));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
24
src/utils/vueWrapHtmlElement.js
Normal file
24
src/utils/vueWrapHtmlElement.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { defineComponent, h, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compatibility wrapper for wrapping an HTMLElement in a Vue component.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
* @returns {import('vue').Component}
|
||||||
|
*/
|
||||||
|
export default function vueWrapHtmlElement(element) {
|
||||||
|
return defineComponent({
|
||||||
|
setup() {
|
||||||
|
const wrapper = ref(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (wrapper.value) {
|
||||||
|
wrapper.value.appendChild(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render function returning the wrapper div
|
||||||
|
return () => h('div', { ref: wrapper });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
Reference in New Issue
Block a user