Skip to content

Commit

Permalink
several small Ditto UI improvements
Browse files Browse the repository at this point in the history
* add a tab "Message to Thing" to send thing messages
* add a loading spinner to the "Send" (message) button and deactivate it while sending
* update a complete Thing using "PATCH" and with the new 3.4.0 header "if-equal: skip-minimizing-merge"
* only send eTag if it could be retrieved when updating complete thing
* added missing "ilike" predicate to the search slot

Signed-off-by: Thomas Jäckle <thomas.jaeckle@beyonnex.io>
  • Loading branch information
thjaeckle committed Oct 13, 2023
1 parent 87103b4 commit 3a3d59f
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 40 deletions.
10 changes: 9 additions & 1 deletion ui/main.scss
Expand Up @@ -195,4 +195,12 @@ h5>.badge {

.autoComplete_wrapper ul > li[aria-selected="true"] {
background-color: rgba(123, 123, 123, 0.1);
}
}

.spinner-border {
visibility:hidden;
}

button.busy .spinner-border {
visibility:visible !important;
}
23 changes: 13 additions & 10 deletions ui/main.ts
Expand Up @@ -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;

Expand All @@ -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();
Expand Down
6 changes: 4 additions & 2 deletions ui/modules/api.ts
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
},
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions ui/modules/things/featureMessages.html
Expand Up @@ -19,6 +19,7 @@
<button class="btn btn-outline-secondary btn-sm" id="buttonMessageSend">
<i class="bi bi-send"></i>
Send
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</button>
<div class="invalid-feedback"></div>
</div>
Expand Down
26 changes: 16 additions & 10 deletions 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;

Expand Down Expand Up @@ -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();
};

Expand Down Expand Up @@ -113,18 +115,22 @@ 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 +
'/inbox/messages/' + dom.inputMessageSubject.value +
'?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('');
});
}
Expand All @@ -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 = '';
}
Expand Down
52 changes: 52 additions & 0 deletions ui/modules/things/thingMessages.html
@@ -0,0 +1,52 @@
<!--
~ 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
-->
<div class="tab-pane container fade no-margin">
<div class="resizable_flex_column">
<div class="input-group input-group-sm mb-1 mt-1 has-validation">
<label class="input-group-text">Subject and Timeout</label>
<input type="text" class="form-control" id="inputThingMessageSubject"></input>
<input type="number" class="form-control form-control-sm" id="inputThingMessageTimeout" value="10"></input>
<button class="btn btn-outline-secondary btn-sm" id="buttonThingMessageSend">
<i class="bi bi-send"></i>
Send
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</button>
<div class="invalid-feedback"></div>
</div>
<div class="input-group input-group-sm mb-1 has-validation">
<label class="input-group-text">Template</label>
<div class="btn-group dropend">
<button id="buttonThingMessageFavorite" class="btn btn-outline-secondary btn-sm" data-bs-toggle="tooltip"
title="Save favorite for message on this feature">
<i id="favIconThingMessage" class="bi bi-star"></i>
</button>
<button class="btn btn-outline-secondary btn-sm dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown"></button>
<ul id="ulThingMessageTemplates" class="dropdown-menu" style="position: fixed; top: auto;">
</ul>
</div>
<input type="text" class="form-control" id="inputThingMessageTemplate">
<div class="invalid-feedback"></div>
</div>
<div class="input-group input-group-sm" style="flex-grow: 1; display: flex;">
<label class="input-group-text">Payload<br>and<br>Response</label>
<div class="ace_container" style="flex-grow: 1;">
<div class="script_editor" id="acePayloadThingMessage"></div>
</div>
<div class="ace_container" style="flex-grow: 1;">
<div class="script_editor" id="aceResponseThingMessage"></div>
</div>
</div>

</div>
</div>
159 changes: 159 additions & 0 deletions 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['/']),
);
}
}

0 comments on commit 3a3d59f

Please sign in to comment.