diff --git a/ui/main.scss b/ui/main.scss
index 7628548241..40b03164f3 100644
--- a/ui/main.scss
+++ b/ui/main.scss
@@ -195,4 +195,12 @@ h5>.badge {
.autoComplete_wrapper ul > li[aria-selected="true"] {
background-color: rgba(123, 123, 123, 0.1);
-}
\ No newline at end of file
+}
+
+.spinner-border {
+ visibility:hidden;
+}
+
+button.busy .spinner-border {
+ visibility:visible !important;
+}
diff --git a/ui/main.ts b/ui/main.ts
index f1b2c43a1e..c37729357f 100644
--- a/ui/main.ts
+++ b/ui/main.ts
@@ -10,31 +10,33 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
+import { Dropdown } from 'bootstrap';
/* eslint-disable new-cap */
import 'bootstrap/dist/css/bootstrap.min.css';
import './main.scss';
-import {Dropdown} from 'bootstrap';
+import * as Connections from './modules/connections/connections.js';
+import * as ConnectionsCRUD from './modules/connections/connectionsCRUD.js';
+import * as ConnectionsMonitor from './modules/connections/connectionsMonitor.js';
import * as Authorization from './modules/environments/authorization.js';
import * as Environments from './modules/environments/environments.js';
+import * as Operations from './modules/operations/operations.js';
+import * as Policies from './modules/policies/policies.js';
import * as Attributes from './modules/things/attributes.js';
-import * as Features from './modules/things/features.js';
import * as FeatureMessages from './modules/things/featureMessages.js';
+import * as Features from './modules/things/features.js';
import * as Fields from './modules/things/fields.js';
+import * as MessagesIncoming from './modules/things/messagesIncoming.js';
import * as SearchFilter from './modules/things/searchFilter.js';
+import * as ThingMessages from './modules/things/thingMessages.js';
import * as Things from './modules/things/things.js';
-import * as ThingsSearch from './modules/things/thingsSearch.js';
import * as ThingsCRUD from './modules/things/thingsCRUD.js';
+import * as ThingsSearch from './modules/things/thingsSearch.js';
import * as ThingsSSE from './modules/things/thingsSSE.js';
-import * as MessagesIncoming from './modules/things/messagesIncoming.js';
-import * as Connections from './modules/connections/connections.js';
-import * as ConnectionsCRUD from './modules/connections/connectionsCRUD.js';
-import * as ConnectionsMonitor from './modules/connections/connectionsMonitor.js';
-import * as Operations from './modules/operations/operations.js';
-import * as Policies from './modules/policies/policies.js';
+import { WoTDescription } from './modules/things/wotDescription.js';
import * as Utils from './modules/utils.js';
-import {WoTDescription} from './modules/things/wotDescription.js';
import './modules/utils/crudToolbar.js';
+
let resized = false;
let mainNavbar;
@@ -43,6 +45,7 @@ document.addEventListener('DOMContentLoaded', async function() {
await Things.ready();
ThingsSearch.ready();
ThingsCRUD.ready();
+ await ThingMessages.ready();
ThingsSSE.ready();
MessagesIncoming.ready();
Attributes.ready();
diff --git a/ui/modules/api.ts b/ui/modules/api.ts
index f13602c222..37144031b1 100644
--- a/ui/modules/api.ts
+++ b/ui/modules/api.ts
@@ -12,9 +12,9 @@
* SPDX-License-Identifier: EPL-2.0
*/
+import { EventSourcePolyfill } from 'event-source-polyfill';
import * as Environments from './environments/environments.js';
import * as Utils from './utils.js';
-import {EventSourcePolyfill} from 'event-source-polyfill';
const config = {
@@ -320,11 +320,12 @@ export function setAuthHeader(forDevOps) {
export async function callDittoREST(method, path, body = null,
additionalHeaders = null, returnHeaders = false, devOps = false) {
let response;
+ const contentType = method === 'PATCH' ? 'application/merge-patch+json' : 'application/json';
try {
response = await fetch(Environments.current().api_uri + (devOps ? '' : '/api/2') + path, {
method: method,
headers: {
- 'Content-Type': 'application/json',
+ 'Content-Type': contentType,
[authHeaderKey]: authHeaderValue,
...additionalHeaders,
},
@@ -345,6 +346,7 @@ export async function callDittoREST(method, path, body = null,
throw new Error('An error occurred: ' + response.status);
}
if (response.status !== 204) {
+ console.log(...response.headers);
if (returnHeaders) {
return response;
} else {
diff --git a/ui/modules/things/featureMessages.html b/ui/modules/things/featureMessages.html
index c35f30fd50..1f4d54cd1f 100644
--- a/ui/modules/things/featureMessages.html
+++ b/ui/modules/things/featureMessages.html
@@ -19,6 +19,7 @@
diff --git a/ui/modules/things/featureMessages.ts b/ui/modules/things/featureMessages.ts
index 39d75ca854..cc748e8054 100644
--- a/ui/modules/things/featureMessages.ts
+++ b/ui/modules/things/featureMessages.ts
@@ -1,23 +1,23 @@
/*
-* Copyright (c) 2022 Contributors to the Eclipse Foundation
-*
-* See the NOTICE file(s) distributed with this work for additional
+ * Copyright (c) 2022 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
-*
-* SPDX-License-Identifier: EPL-2.0
-*/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
/* eslint-disable require-jsdoc */
import * as API from '../api.js';
import * as Environments from '../environments/environments.js';
import * as Utils from '../utils.js';
-import * as Things from './things.js';
-import * as Features from './features.js';
import featureMessagesHTML from './featureMessages.html';
+import * as Features from './features.js';
+import * as Things from './things.js';
let theFeatureId;
@@ -59,6 +59,8 @@ export async function ready() {
Utils.assert(theFeatureId, 'Please select a Feature', dom.tableValidationFeature);
Utils.assert(dom.inputMessageSubject.value, 'Please give a Subject', dom.inputMessageSubject);
Utils.assert(dom.inputMessageTimeout.value, 'Please give a timeout', dom.inputMessageTimeout);
+ dom.buttonMessageSend.classList.add('busy');
+ dom.buttonMessageSend.disabled = true;
messageFeature();
};
@@ -113,7 +115,7 @@ export async function ready() {
* Calls Ditto to send a message with the parameters of the fields in the UI
*/
function messageFeature() {
- const payload = JSON.parse(acePayload.getValue());
+ const payload = acePayload && acePayload.getValue().length > 0 && JSON.parse(acePayload.getValue());
aceResponse.setValue('');
API.callDittoREST('POST', '/things/' + Things.theThing.thingId +
'/features/' + theFeatureId +
@@ -121,10 +123,14 @@ function messageFeature() {
'?timeout=' + dom.inputMessageTimeout.value,
payload,
).then((data) => {
+ dom.buttonMessageSend.classList.remove('busy');
+ dom.buttonMessageSend.disabled = false;
if (dom.inputMessageTimeout.value > 0) {
aceResponse.setValue(JSON.stringify(data, null, 2), -1);
}
}).catch((err) => {
+ dom.buttonMessageSend.classList.remove('busy');
+ dom.buttonMessageSend.disabled = false;
aceResponse.setValue('');
});
}
@@ -145,7 +151,7 @@ function clearAllFields() {
dom.inputMessageTemplate.value = null;
dom.inputMessageSubject.value = null;
dom.inputMessageTimeout.value = '10';
- acePayload.setValue('{}');
+ acePayload.setValue('');
aceResponse.setValue('');
dom.ulMessageTemplates.innerHTML = '';
}
diff --git a/ui/modules/things/thingMessages.html b/ui/modules/things/thingMessages.html
new file mode 100644
index 0000000000..d812150fd5
--- /dev/null
+++ b/ui/modules/things/thingMessages.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/modules/things/thingMessages.ts b/ui/modules/things/thingMessages.ts
new file mode 100644
index 0000000000..fa3a611e46
--- /dev/null
+++ b/ui/modules/things/thingMessages.ts
@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+/* eslint-disable require-jsdoc */
+import * as API from '../api.js';
+import * as Environments from '../environments/environments.js';
+import * as Utils from '../utils.js';
+import messagesHTML from './thingMessages.html';
+import * as Things from './things.js';
+
+const dom = {
+ inputThingMessageSubject: null,
+ inputThingMessageTimeout: null,
+ inputThingMessageTemplate: null,
+ buttonThingMessageSend: null,
+ buttonThingMessageFavorite: null,
+ ulThingMessageTemplates: null,
+ favIconThingMessage: null,
+};
+
+let acePayload;
+let aceResponse;
+
+/**
+ * Initializes components. Should be called after DOMContentLoaded event
+ */
+export async function ready() {
+ Environments.addChangeListener(onEnvironmentChanged);
+
+ Utils.addTab(
+ document.getElementById('tabItemsThing'),
+ document.getElementById('tabContentThing'),
+ 'Message to Thing',
+ messagesHTML,
+ );
+
+ Utils.getAllElementsById(dom);
+
+ acePayload = Utils.createAceEditor('acePayloadThingMessage', 'ace/mode/json');
+ aceResponse = Utils.createAceEditor('aceResponseThingMessage', 'ace/mode/json', true);
+
+
+ dom.buttonThingMessageSend.onclick = () => {
+ Utils.assert(dom.inputThingMessageSubject.value, 'Please give a Subject', dom.inputThingMessageSubject);
+ Utils.assert(dom.inputThingMessageTimeout.value, 'Please give a timeout', dom.inputThingMessageTimeout);
+ dom.buttonThingMessageSend.classList.add('busy');
+ dom.buttonThingMessageSend.disabled = true;
+ messageThing();
+ };
+
+ dom.buttonThingMessageFavorite.onclick = () => {
+ const templateName = dom.inputThingMessageTemplate.value;
+ Utils.assert(templateName, 'Please give a name for the template', dom.inputThingMessageTemplate);
+ Environments.current().messageTemplates['/'] = Environments.current().messageTemplates['/'] || {};
+ if (Object.keys(Environments.current().messageTemplates['/']).includes(templateName) &&
+ dom.favIconThingMessage.classList.contains('bi-star-fill')) {
+ dom.favIconThingMessage.classList.replace('bi-star-fill', 'bi-star');
+ delete Environments.current().messageTemplates['/'][templateName];
+ } else {
+ dom.favIconThingMessage.classList.replace('bi-star', 'bi-star-fill');
+ Environments.current().messageTemplates['/'][templateName] = {
+ subject: dom.inputThingMessageSubject.value,
+ timeout: dom.inputThingMessageTimeout.value,
+ payload: JSON.parse(acePayload.getValue()),
+ };
+ acePayload.session.getUndoManager().markClean();
+ }
+ Environments.environmentsJsonChanged('messageTemplates');
+ };
+
+ dom.ulThingMessageTemplates.addEventListener('click', (event) => {
+ if (event.target && event.target.classList.contains('dropdown-item')) {
+ dom.favIconThingMessage.classList.replace('bi-star', 'bi-star-fill');
+ const template = Environments.current().messageTemplates['/'][event.target.textContent];
+ dom.inputThingMessageTemplate.value = event.target.textContent;
+ dom.inputThingMessageSubject.value = template.subject;
+ dom.inputThingMessageTimeout.value = template.timeout;
+ acePayload.setValue(JSON.stringify(template.payload, null, 2), -1);
+ acePayload.session.getUndoManager().markClean();
+ }
+ });
+
+ [dom.inputThingMessageTemplate, dom.inputThingMessageSubject, dom.inputThingMessageTimeout].forEach((e) => {
+ e.addEventListener('change', () => {
+ dom.favIconThingMessage.classList.replace('bi-star-fill', 'bi-star');
+ });
+ });
+
+ acePayload.on('input', () => {
+ if (!acePayload.session.getUndoManager().isClean()) {
+ dom.favIconThingMessage.classList.replace('bi-star-fill', 'bi-star');
+ }
+ });
+}
+
+/**
+ * Calls Ditto to send a message with the parameters of the fields in the UI
+ */
+function messageThing() {
+ const payload = acePayload && acePayload.getValue().length > 0 && JSON.parse(acePayload.getValue());
+ aceResponse.setValue('');
+ API.callDittoREST('POST', '/things/' + Things.theThing.thingId +
+ '/inbox/messages/' + dom.inputThingMessageSubject.value +
+ '?timeout=' + dom.inputThingMessageTimeout.value,
+ payload,
+ ).then((data) => {
+ dom.buttonThingMessageSend.classList.remove('busy');
+ dom.buttonThingMessageSend.disabled = false;
+ if (dom.inputThingMessageTimeout.value > 0) {
+ aceResponse.setValue(JSON.stringify(data, null, 2), -1);
+ }
+ }).catch((err) => {
+ dom.buttonThingMessageSend.classList.remove('busy');
+ dom.buttonThingMessageSend.disabled = false;
+ aceResponse.setValue('');
+ });
+}
+
+function onEnvironmentChanged(modifiedField) {
+ Environments.current()['messageTemplates'] = Environments.current()['messageTemplates'] || {};
+
+ if (!modifiedField) {
+ clearAllFields();
+ }
+ if (modifiedField === 'messageTemplates') {
+ refillTemplates();
+ }
+}
+
+function clearAllFields() {
+ dom.favIconThingMessage.classList.replace('bi-star-fill', 'bi-star');
+ dom.inputThingMessageTemplate.value = null;
+ dom.inputThingMessageSubject.value = null;
+ dom.inputThingMessageTimeout.value = '10';
+ acePayload.setValue('');
+ aceResponse.setValue('');
+ dom.ulThingMessageTemplates.innerHTML = '';
+}
+
+function refillTemplates() {
+ dom.ulThingMessageTemplates.innerHTML = '';
+ Utils.addDropDownEntries(dom.ulThingMessageTemplates, ['Saved message templates'], true);
+ if (Environments.current().messageTemplates['/']) {
+ Utils.addDropDownEntries(
+ dom.ulThingMessageTemplates,
+ Object.keys(Environments.current().messageTemplates['/']),
+ );
+ }
+}
diff --git a/ui/modules/things/thingsCRUD.ts b/ui/modules/things/thingsCRUD.ts
index 3ea9e4f0e4..4cca929bf9 100644
--- a/ui/modules/things/thingsCRUD.ts
+++ b/ui/modules/things/thingsCRUD.ts
@@ -1,22 +1,22 @@
/*
-* Copyright (c) 2022 Contributors to the Eclipse Foundation
-*
-* See the NOTICE file(s) distributed with this work for additional
-* information regarding copyright ownership.
-*
-* This program and the accompanying materials are made available under the
-* terms of the Eclipse Public License 2.0 which is available at
-* http://www.eclipse.org/legal/epl-2.0
-*
-* SPDX-License-Identifier: EPL-2.0
-*/
+ * Copyright (c) 2022 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
/* eslint-disable require-jsdoc */
// @ts-check
import * as API from '../api.js';
import * as Utils from '../utils.js';
-import * as ThingsSearch from './thingsSearch.js';
import * as Things from './things.js';
+import * as ThingsSearch from './thingsSearch.js';
import thingTemplates from './thingTemplates.json';
let thingJsonEditor;
@@ -67,9 +67,10 @@ function onDeleteThingClick() {
}
function onUpdateThingClick() {
- API.callDittoREST('PUT', `/things/${dom.crudThings.idValue}`, JSON.parse(thingJsonEditor.getValue()),
+ API.callDittoREST('PATCH', `/things/${dom.crudThings.idValue}`, JSON.parse(thingJsonEditor.getValue()),
{
- 'if-match': eTag,
+ 'if-match': eTag || "*",
+ 'if-equal': 'skip-minimizing-merge'
},
).then(() => {
dom.crudThings.toggleEdit();
diff --git a/ui/modules/things/thingsSearch.ts b/ui/modules/things/thingsSearch.ts
index d1f8f53724..a2c8dcb460 100644
--- a/ui/modules/things/thingsSearch.ts
+++ b/ui/modules/things/thingsSearch.ts
@@ -16,15 +16,15 @@
/* eslint-disable no-invalid-this */
/* eslint-disable arrow-parens */
-import {JSONPath} from 'jsonpath-plus';
+import { JSONPath } from 'jsonpath-plus';
import * as API from '../api.js';
+import * as Environments from '../environments/environments.js';
import * as Utils from '../utils.js';
import * as Fields from './fields.js';
import * as Things from './things.js';
import * as ThingsSSE from './thingsSSE.js';
-import * as Environments from '../environments/environments.js';
let lastSearch = '';
let theSearchCursor;
@@ -78,7 +78,7 @@ function onThingsTableClicked(event) {
*/
export function searchTriggered(filter) {
lastSearch = filter;
- const regex = /^(eq\(|ne\(|gt\(|ge\(|lt\(|le\(|in\(|like\(|exists\(|and\(|or\(|not\().*/;
+ const regex = /^(eq\(|ne\(|gt\(|ge\(|lt\(|le\(|in\(|like\(|ilike\(|exists\(|and\(|or\(|not\().*/;
if (filter === '' || regex.test(filter)) {
searchThings(filter);
} else {