diff --git a/.cspell.json b/.cspell.json
index 31df39a88e..bc37d13deb 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -493,6 +493,7 @@
"WCAG",
"stackedplot",
"Andale",
+ "unnnormalized",
"checksnapshots",
"specced"
],
diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js
index a5ee4bc6e6..750784b98c 100644
--- a/src/api/objects/ObjectAPI.js
+++ b/src/api/objects/ObjectAPI.js
@@ -354,6 +354,9 @@ export default class ObjectAPI {
isPersistable(idOrKeyString) {
let identifier = utils.parseKeyString(idOrKeyString);
let provider = this.getProvider(identifier);
+ if (provider?.isReadOnly) {
+ return !provider.isReadOnly();
+ }
return provider !== undefined && provider.create !== undefined && provider.update !== undefined;
}
diff --git a/src/plugins/CouchDBSearchFolder/pluginSpec.js b/src/plugins/CouchDBSearchFolder/pluginSpec.js
index a41bd080bb..21d2ee9339 100644
--- a/src/plugins/CouchDBSearchFolder/pluginSpec.js
+++ b/src/plugins/CouchDBSearchFolder/pluginSpec.js
@@ -38,7 +38,6 @@ describe('the plugin', function () {
let couchPlugin = openmct.plugins.CouchDB(testPath);
openmct.install(couchPlugin);
-
openmct.install(
new CouchDBSearchFolderPlugin('CouchDB Documents', couchPlugin, {
selector: {
diff --git a/src/plugins/localStorage/LocalStorageObjectProvider.js b/src/plugins/localStorage/LocalStorageObjectProvider.js
index 4f3fc8464c..ca6f60dd8b 100644
--- a/src/plugins/localStorage/LocalStorageObjectProvider.js
+++ b/src/plugins/localStorage/LocalStorageObjectProvider.js
@@ -74,6 +74,10 @@ export default class LocalStorageObjectProvider {
this.localStorage.setItem(this.spaceKey, JSON.stringify(space));
}
+ isReadOnly() {
+ return false;
+ }
+
/**
* @private
*/
diff --git a/src/plugins/myItems/README.md b/src/plugins/myItems/README.md
index df61bacf6c..e3582a3369 100644
--- a/src/plugins/myItems/README.md
+++ b/src/plugins/myItems/README.md
@@ -1,8 +1,26 @@
# My Items plugin
-Defines top-level folder named "My Items" to store user-created items. Enabled by default, this can be disabled in a
-read-only deployment with no user-editable objects.
+Defines top-level folder named "My Items" to store user-created items. Enabled by default, this can be disabled in a read-only deployment with no user-editable objects.
## Installation
```js
openmct.install(openmct.plugins.MyItems());
+```
+
+## Options
+When installing, the plugin can take several options:
+
+- `name`: The label of the root object. Defaults to "My Items"
+ - Example: `'Apple Items'`
+
+- `namespace`: The namespace to create the root object in. Defaults to the empty string `''`
+ - Example: `'apple-namespace'`
+
+- `priority`: The optional priority to install this plugin. Defaults to `openmct.priority.LOW`
+ - Example: `'openmct.priority.LOW'`
+
+E.g., to install with a custom name and namespace, you could use:
+
+
+```js
+openmct.install(openmct.plugins.MyItems('Apple Items', 'apple-namespace'));
```
\ No newline at end of file
diff --git a/src/plugins/myItems/myItemsInterceptor.js b/src/plugins/myItems/myItemsInterceptor.js
index 49b9a4eb94..d1efca1ed6 100644
--- a/src/plugins/myItems/myItemsInterceptor.js
+++ b/src/plugins/myItems/myItemsInterceptor.js
@@ -20,9 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
-import { MY_ITEMS_KEY } from './createMyItemsIdentifier.js';
-
-function myItemsInterceptor(openmct, identifierObject, name) {
+function myItemsInterceptor({ openmct, identifierObject, name }) {
const myItemsModel = {
identifier: identifierObject,
name,
@@ -33,7 +31,10 @@ function myItemsInterceptor(openmct, identifierObject, name) {
return {
appliesTo: (identifier) => {
- return identifier.key === MY_ITEMS_KEY;
+ return (
+ identifier.key === myItemsModel.identifier.key &&
+ identifier.namespace === myItemsModel.identifier.namespace
+ );
},
invoke: (identifier, object) => {
if (!object || openmct.objects.isMissing(object)) {
diff --git a/src/plugins/myItems/plugin.js b/src/plugins/myItems/plugin.js
index 9bb59e8466..44bed8f23e 100644
--- a/src/plugins/myItems/plugin.js
+++ b/src/plugins/myItems/plugin.js
@@ -31,13 +31,13 @@ export default function MyItemsPlugin(
priority = undefined
) {
return function install(openmct) {
- const identifier = createMyItemsIdentifier(namespace);
+ const identifierObject = createMyItemsIdentifier(namespace);
if (priority === undefined) {
priority = openmct.priority.LOW;
}
- openmct.objects.addGetInterceptor(myItemsInterceptor(openmct, identifier, name));
- openmct.objects.addRoot(identifier, priority);
+ openmct.objects.addGetInterceptor(myItemsInterceptor({ openmct, identifierObject, name }));
+ openmct.objects.addRoot(identifierObject, priority);
};
}
diff --git a/src/plugins/persistence/couch/CouchChangesFeed.js b/src/plugins/persistence/couch/CouchChangesFeed.js
index d47429ba28..6150c57d30 100644
--- a/src/plugins/persistence/couch/CouchChangesFeed.js
+++ b/src/plugins/persistence/couch/CouchChangesFeed.js
@@ -80,7 +80,7 @@
keepAliveTimer = setTimeout(self.listenForChanges, keepAliveTime);
if (!couchEventSource || couchEventSource.readyState === EventSource.CLOSED) {
- console.debug('⇿ Opening CouchDB change feed connection ⇿');
+ console.debug(`⇿ Opening CouchDB change feed connection for ${changesFeedUrl} ⇿`);
couchEventSource = new EventSource(changesFeedUrl);
couchEventSource.onerror = self.onerror;
couchEventSource.onopen = self.onopen;
@@ -88,7 +88,7 @@
// start listening for events
couchEventSource.addEventListener('message', self.onCouchMessage);
connected = true;
- console.debug('⇿ Opened connection ⇿');
+ console.debug(`⇿ Opened connection to ${changesFeedUrl} ⇿`);
}
};
diff --git a/src/plugins/persistence/couch/CouchObjectProvider.js b/src/plugins/persistence/couch/CouchObjectProvider.js
index 38e1ee4d5e..ab1859f092 100644
--- a/src/plugins/persistence/couch/CouchObjectProvider.js
+++ b/src/plugins/persistence/couch/CouchObjectProvider.js
@@ -33,13 +33,13 @@ const HEARTBEAT = 50000;
const ALL_DOCS = '_all_docs?include_docs=true';
class CouchObjectProvider {
- constructor(openmct, options, namespace, indicator) {
- options = this.#normalize(options);
+ constructor({ openmct, databaseConfiguration, couchStatusIndicator }) {
this.openmct = openmct;
- this.indicator = indicator;
- this.url = options.url;
- this.useDesignDocuments = options.useDesignDocuments;
- this.namespace = namespace;
+ this.indicator = couchStatusIndicator;
+ this.url = databaseConfiguration.url;
+ this.readOnly = databaseConfiguration.readOnly;
+ this.useDesignDocuments = databaseConfiguration.useDesignDocuments;
+ this.namespace = databaseConfiguration.namespace;
this.objectQueue = {};
this.observers = {};
this.batchIds = [];
@@ -47,6 +47,7 @@ class CouchObjectProvider {
this.onEventError = this.onEventError.bind(this);
this.flushPersistenceQueue = _.debounce(this.flushPersistenceQueue.bind(this));
this.persistenceQueue = [];
+ this.rootObject = null;
}
/**
@@ -59,7 +60,10 @@ class CouchObjectProvider {
// eslint-disable-next-line no-undef
const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}couchDBChangesFeed.js`;
- sharedWorker = new SharedWorker(sharedWorkerURL, 'CouchDB SSE Shared Worker');
+ sharedWorker = new SharedWorker(
+ sharedWorkerURL,
+ `CouchDB SSE Shared Worker for ${this.namespace}`
+ );
sharedWorker.port.onmessage = provider.onSharedWorkerMessage.bind(this);
sharedWorker.port.onmessageerror = provider.onSharedWorkerMessageError.bind(this);
sharedWorker.port.start();
@@ -93,7 +97,7 @@ class CouchObjectProvider {
this.changesFeedSharedWorkerConnectionId = event.data.connectionId;
} else if (event.data.type === 'state') {
const state = this.#messageToIndicatorState(event.data.state);
- this.indicator.setIndicatorToState(state);
+ this.indicator?.setIndicatorToState(state);
} else {
let objectChanges = event.data.objectChanges;
const objectIdentifier = {
@@ -184,16 +188,8 @@ class CouchObjectProvider {
return state;
}
- //backwards compatibility, options used to be a url. Now it's an object
- #normalize(options) {
- if (typeof options === 'string') {
- return {
- url: options,
- useDesignDocuments: false
- };
- }
-
- return options;
+ isReadOnly() {
+ return this.readOnly;
}
async request(subPath, method, body, signal) {
@@ -233,7 +229,7 @@ class CouchObjectProvider {
// Network error, CouchDB unreachable.
if (response === null) {
- this.indicator.setIndicatorToState(DISCONNECTED);
+ this.indicator?.setIndicatorToState(DISCONNECTED);
console.error(error.message);
throw new Error(`CouchDB Error - No response"`);
@@ -256,7 +252,7 @@ class CouchObjectProvider {
* @private
*/
#handleResponseCode(status, json, fetchOptions) {
- this.indicator.setIndicatorToState(this.#statusCodeToIndicatorState(status));
+ this.indicator?.setIndicatorToState(this.#statusCodeToIndicatorState(status));
if (status === CouchObjectProvider.HTTP_CONFLICT) {
const objectName = JSON.parse(fetchOptions.body)?.model?.name;
throw new this.openmct.objects.errors.Conflict(`Conflict persisting "${objectName}"`);
@@ -684,7 +680,7 @@ class CouchObjectProvider {
}
const indicatorState = this.#messageToIndicatorState(message);
- this.indicator.setIndicatorToState(indicatorState);
+ this.indicator?.setIndicatorToState(indicatorState);
}
/**
diff --git a/src/plugins/persistence/couch/CouchSearchProvider.js b/src/plugins/persistence/couch/CouchSearchProvider.js
index 478f25dd9e..7c0740eff3 100644
--- a/src/plugins/persistence/couch/CouchSearchProvider.js
+++ b/src/plugins/persistence/couch/CouchSearchProvider.js
@@ -51,6 +51,10 @@ class CouchSearchProvider {
return this.supportedSearchTypes.includes(searchType);
}
+ isReadOnly() {
+ return true;
+ }
+
search(query, abortSignal, searchType) {
if (searchType === this.searchTypes.OBJECTS) {
return this.searchForObjects(query, abortSignal);
diff --git a/src/plugins/persistence/couch/README.md b/src/plugins/persistence/couch/README.md
index c01c72d42e..29421d45ff 100644
--- a/src/plugins/persistence/couch/README.md
+++ b/src/plugins/persistence/couch/README.md
@@ -153,13 +153,63 @@ sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.s
Add a line to install the CouchDB plugin for Open MCT:
```js
- openmct.install(openmct.plugins.CouchDB({url: "http://localhost:5984/openmct", useDesignDocuments: false}));
+ openmct.install(
+ openmct.plugins.CouchDB({
+ databases: [
+ {
+ url: 'http://localhost:5984/openmct',
+ namespace: '',
+ additionalNamespaces: [],
+ readOnly: false,
+ useDesignDocuments: false,
+ indicator: true
+ }
+ ]
+ })
+ );
```
+### Configuration Options for OpenMCT
+
+When installing the CouchDB plugin for OpenMCT, you can specify a list of databases with configuration options for each. Here's a breakdown of the available options for each database:
+
+- `url`: The URL to the CouchDB instance, specifying the protocol, hostname, and port as needed.
+ - Example: `'http://localhost:5984/openmct'`
+
+- `namespace`: The namespace associated with this database.
+ - Example: `'openmct-sandbox'`
+
+- `additionalNamespaces`: Other namespaces that this plugin should respond to requests for.
+ - Example: `['apple-namespace', 'pear-namespace']`
+
+- `readOnly`: A boolean indicating whether the database should be treated as read-only. If set to `true`, OpenMCT will not attempt to write to this database.
+ - Example: `false`
+
+- `useDesignDocuments`: Indicates whether design documents should be used to speed up annotation search.
+ - Example: `false`
+
+- `indicator`: A boolean to specify whether an indicator should show the status of this CouchDB connection in the OpenMCT interface.
+ - Example: `true`
+
+Note: If using the `exampleTags` plugin with non-blank namespaces, you'll need to configure it point to a writable database. For example:
+
+```js
+openmct.install(
+ openmct.plugins.example.ExampleTags({ namespaceToSaveAnnotations: 'openmct-sandbox' })
+ );
+```
+
+Note: If using the `MyItems` plugin, be sure to configure a root for each writable namespace. E.g., if you have two namespaces called `apple-namespace` and `pear-namespace`:
+```js
+ openmct.install(openmct.plugins.MyItems('Apple Items', 'apple-namespace'));
+ openmct.install(openmct.plugins.MyItems('Pear Items', 'pear-namespace'));
+```
+This will create a root object with the id of `mine` in both namespaces upon load if not already created.
+
# Validating a successful Installation
1. Start Open MCT by running `npm start` in the `openmct` path.
-2. Navigate to and create a random object in Open MCT (e.g., a 'Clock') and save. You may get an error saying that the object failed to persist - this is a known error that you can ignore, and will only happen the first time you save - just try again.
+2. Navigate to and create a random object in Open MCT (e.g., a 'Clock') and save.
3. Navigate to:
4. Look at the 'JSON' tab and ensure you can see the specific object you created above.
5. All done! 🏆
@@ -242,4 +292,4 @@ To enable them in Open MCT, we need to configure the plugin `useDesignDocuments`
```js
openmct.install(openmct.plugins.CouchDB({url: "http://localhost:5984/openmct", useDesignDocuments: true}));
- ```
\ No newline at end of file
+ ```
diff --git a/src/plugins/persistence/couch/plugin.js b/src/plugins/persistence/couch/plugin.js
index 19b8a9b625..a1c7f6c661 100644
--- a/src/plugins/persistence/couch/plugin.js
+++ b/src/plugins/persistence/couch/plugin.js
@@ -24,29 +24,81 @@ import CouchObjectProvider from './CouchObjectProvider.js';
import CouchSearchProvider from './CouchSearchProvider.js';
import CouchStatusIndicator from './CouchStatusIndicator.js';
-const NAMESPACE = '';
+const DEFAULT_NAMESPACE = '';
const LEGACY_SPACE = 'mct';
-const COUCH_SEARCH_ONLY_NAMESPACE = `COUCH_SEARCH_${Date.now()}`;
export default function CouchPlugin(options) {
- return function install(openmct) {
- const simpleIndicator = openmct.indicators.simpleIndicator();
- openmct.indicators.add(simpleIndicator);
- const couchStatusIndicator = new CouchStatusIndicator(simpleIndicator);
- install.couchProvider = new CouchObjectProvider(
- openmct,
- options,
- NAMESPACE,
- couchStatusIndicator
- );
+ function normalizeOptions(unnnormalizedOptions) {
+ const normalizedOptions = {};
+ if (typeof unnnormalizedOptions === 'string') {
+ normalizedOptions.databases = [
+ {
+ url: options,
+ namespace: DEFAULT_NAMESPACE,
+ additionalNamespaces: [LEGACY_SPACE],
+ readOnly: false,
+ useDesignDocuments: false,
+ indicator: true
+ }
+ ];
+ } else if (!unnnormalizedOptions.databases) {
+ normalizedOptions.databases = [
+ {
+ url: unnnormalizedOptions.url,
+ namespace: DEFAULT_NAMESPACE,
+ additionalNamespaces: [LEGACY_SPACE],
+ readOnly: false,
+ useDesignDocuments: unnnormalizedOptions.useDesignDocuments,
+ indicator: true
+ }
+ ];
+ } else {
+ normalizedOptions.databases = unnnormalizedOptions.databases;
+ }
- // Unfortunately, for historical reasons, Couch DB produces objects with a mix of namespaces (alternately "mct", and "")
- // Installing the same provider under both namespaces means that it can respond to object gets for both namespaces.
- openmct.objects.addProvider(LEGACY_SPACE, install.couchProvider);
- openmct.objects.addProvider(NAMESPACE, install.couchProvider);
- openmct.objects.addProvider(
- COUCH_SEARCH_ONLY_NAMESPACE,
- new CouchSearchProvider(install.couchProvider)
- );
+ // final sanity check, ensure we have all options
+ normalizedOptions.databases.forEach((databaseConfiguration) => {
+ if (!databaseConfiguration.url) {
+ throw new Error(
+ `🛑 CouchDB plugin requires a url option. Please check the configuration for namespace ${databaseConfiguration.namespace}`
+ );
+ } else if (databaseConfiguration.namespace === undefined) {
+ // note we can't check for just !databaseConfiguration.namespace because it could be an empty string
+ throw new Error(
+ `🛑 CouchDB plugin requires a namespace option. Please check the configuration for url ${databaseConfiguration.url}`
+ );
+ }
+ });
+
+ return normalizedOptions;
+ }
+
+ return function install(openmct) {
+ const normalizedOptions = normalizeOptions(options);
+ normalizedOptions.databases.forEach((databaseConfiguration) => {
+ let couchStatusIndicator;
+ if (databaseConfiguration.indicator) {
+ const simpleIndicator = openmct.indicators.simpleIndicator();
+ openmct.indicators.add(simpleIndicator);
+ couchStatusIndicator = new CouchStatusIndicator(simpleIndicator);
+ }
+ // the provider is added to the install function to expose couchProvider to unit tests
+ install.couchProvider = new CouchObjectProvider({
+ openmct,
+ databaseConfiguration,
+ couchStatusIndicator
+ });
+ openmct.objects.addProvider(databaseConfiguration.namespace, install.couchProvider);
+ databaseConfiguration.additionalNamespaces?.forEach((additionalNamespace) => {
+ openmct.objects.addProvider(additionalNamespace, install.couchProvider);
+ });
+
+ // need one search provider for whole couch database
+ const searchOnlyNamespace = `COUCH_SEARCH_${databaseConfiguration.namespace}${Date.now()}`;
+ openmct.objects.addProvider(
+ searchOnlyNamespace,
+ new CouchSearchProvider(install.couchProvider)
+ );
+ });
};
}