Compare commits

..

14 Commits

Author SHA1 Message Date
Nick O'Leary
a6e8fbb54a Ensure palette filter reapplies and clear up unknown categories 2024-03-28 15:21:04 +00:00
Nick O'Leary
28e9ccd372 Merge pull request #4634 from node-red/pr_4623
Retain Palette categories collapsed and filter to localStorage
2024-03-28 14:53:15 +00:00
Nick O'Leary
9a66d9addd Merge pull request #4620 from node-red/pr_4387
Add support for plugin (only) modules to the palette manager
2024-03-28 14:35:33 +00:00
Nick O'Leary
8843bda477 Merge pull request #4628 from node-red/comms-updates
Comms API updates
2024-03-28 14:33:10 +00:00
Nick O'Leary
3278303eec Clear localStorage state on logout 2024-03-28 14:31:31 +00:00
Nick O'Leary
f5fd6e3a36 Rework palette state management 2024-03-28 14:23:39 +00:00
Nick O'Leary
b20c5f3a8d Merge pull request #4621 from GogoVega/addfrench-v4
Add French translations for 4.0.0-beta.1
2024-03-27 17:48:07 +00:00
Nick O'Leary
068b93befa CComms API updates 2024-03-27 17:21:12 +00:00
GogoVega
bffd1d61b2 Improve with error handling, storage cleanup and centralization in one object 2024-03-26 16:58:45 +01:00
GogoVega
4788b81220 Replace setTimeout with a listener 2024-03-26 13:05:38 +01:00
GogoVega
9a07fc03c6 Retain palette collapse and filter to localStorage 2024-03-25 20:47:55 +01:00
GogoVega
954f518030 Add French translations for 4.0.0-beta.1 2024-03-25 12:02:38 +01:00
GogoVega
9f8ff71757 Add French translations for TCP and CSV nodes 2024-03-25 11:58:35 +01:00
GogoVega
06dd59dc81 Add missing global-config translation 2024-03-25 10:51:12 +01:00
13 changed files with 340 additions and 99 deletions

View File

@@ -77,6 +77,53 @@ function CommsConnection(ws, user) {
log.trace("comms.close "+self.session);
removeActiveConnection(self);
});
const handleAuthPacket = function(msg) {
Tokens.get(msg.auth).then(function(client) {
if (client) {
Users.get(client.user).then(function(user) {
if (user) {
self.user = user;
log.audit({event: "comms.auth",user:self.user});
completeConnection(msg, client.scope,msg.auth,true);
} else {
log.audit({event: "comms.auth.fail"});
completeConnection(msg, null,null,false);
}
});
} else {
Users.tokens(msg.auth).then(function(user) {
if (user) {
self.user = user;
log.audit({event: "comms.auth",user:self.user});
completeConnection(msg, user.permissions,msg.auth,true);
} else {
log.audit({event: "comms.auth.fail"});
completeConnection(msg, null,null,false);
}
});
}
});
}
const completeConnection = function(msg, userScope, session, sendAck) {
try {
if (!userScope || !Permissions.hasPermission(userScope,"status.read")) {
ws.send(JSON.stringify({auth:"fail"}));
ws.close();
} else {
pendingAuth = false;
addActiveConnection(self);
self.token = msg.auth;
if (sendAck) {
ws.send(JSON.stringify({auth:"ok"}));
}
}
} catch(err) {
console.log(err.stack);
// Just in case the socket closes before we attempt
// to send anything.
}
}
ws.on('message', function(data,flags) {
var msg = null;
try {
@@ -86,68 +133,34 @@ function CommsConnection(ws, user) {
return;
}
if (!pendingAuth) {
if (msg.subscribe) {
if (msg.auth) {
handleAuthPacket(msg)
} else if (msg.subscribe) {
self.subscribe(msg.subscribe);
// handleRemoteSubscription(ws,msg.subscribe);
} else if (msg.topic) {
runtimeAPI.comms.receive({
user: self.user,
client: self,
topic: msg.topic,
data: msg.data
})
}
} else {
var completeConnection = function(userScope,session,sendAck) {
try {
if (!userScope || !Permissions.hasPermission(userScope,"status.read")) {
ws.send(JSON.stringify({auth:"fail"}));
ws.close();
} else {
pendingAuth = false;
addActiveConnection(self);
self.token = msg.auth;
if (sendAck) {
ws.send(JSON.stringify({auth:"ok"}));
}
}
} catch(err) {
console.log(err.stack);
// Just in case the socket closes before we attempt
// to send anything.
}
}
if (msg.auth) {
Tokens.get(msg.auth).then(function(client) {
if (client) {
Users.get(client.user).then(function(user) {
if (user) {
self.user = user;
log.audit({event: "comms.auth",user:self.user});
completeConnection(client.scope,msg.auth,true);
} else {
log.audit({event: "comms.auth.fail"});
completeConnection(null,null,false);
}
});
} else {
Users.tokens(msg.auth).then(function(user) {
if (user) {
self.user = user;
log.audit({event: "comms.auth",user:self.user});
completeConnection(user.permissions,msg.auth,true);
} else {
log.audit({event: "comms.auth.fail"});
completeConnection(null,null,false);
}
});
}
});
handleAuthPacket(msg)
} else {
if (anonymousUser) {
log.audit({event: "comms.auth",user:anonymousUser});
self.user = anonymousUser;
completeConnection(anonymousUser.permissions,null,false);
completeConnection(msg, anonymousUser.permissions, null, false);
//TODO: duplicated code - pull non-auth message handling out
if (msg.subscribe) {
self.subscribe(msg.subscribe);
}
} else {
log.audit({event: "comms.auth.fail"});
completeConnection(null,null,false);
completeConnection(msg, null,null,false);
}
}
}

View File

@@ -924,7 +924,14 @@
"date": "horodatage",
"jsonata": "expression",
"env": "variable d'environnement",
"cred": "identifiant"
"cred": "identifiant",
"conf-types": "noeud de configuration"
},
"date": {
"format": {
"timestamp": "millisecondes depuis l'époque",
"object": "Objet de date JavaScript"
}
}
},
"editableList": {

View File

@@ -26,6 +26,15 @@ RED.comms = (function() {
var reconnectAttempts = 0;
var active = false;
RED.events.on('login', function(username) {
// User has logged in
// Need to upgrade the connection to be authenticated
if (ws && ws.readyState == 1) {
const auth_tokens = RED.settings.get("auth-tokens");
ws.send(JSON.stringify({auth:auth_tokens.access_token}))
}
})
function connectWS() {
active = true;
var wspath;
@@ -56,6 +65,7 @@ RED.comms = (function() {
ws.send(JSON.stringify({subscribe:t}));
}
}
emit('connect')
}
ws = new WebSocket(wspath);
@@ -180,9 +190,53 @@ RED.comms = (function() {
}
}
function send(topic, msg) {
if (ws && ws.readyState == 1) {
ws.send(JSON.stringify({
topic,
data: msg
}))
}
}
const eventHandlers = {};
function on(evt,func) {
eventHandlers[evt] = eventHandlers[evt]||[];
eventHandlers[evt].push(func);
}
function off(evt,func) {
const handler = eventHandlers[evt];
if (handler) {
for (let i=0;i<handler.length;i++) {
if (handler[i] === func) {
handler.splice(i,1);
return;
}
}
}
}
function emit() {
const evt = arguments[0]
const args = Array.prototype.slice.call(arguments,1);
if (eventHandlers[evt]) {
let cpyHandlers = [...eventHandlers[evt]];
for (let i=0;i<cpyHandlers.length;i++) {
try {
cpyHandlers[i].apply(null, args);
} catch(err) {
console.warn("RED.comms.emit error: ["+evt+"] "+(err.toString()));
console.warn(err);
}
}
}
}
return {
connect: connectWS,
subscribe: subscribe,
unsubscribe:unsubscribe
unsubscribe:unsubscribe,
on,
off,
send
}
})();

View File

@@ -153,10 +153,6 @@ RED.envVar = (function() {
}
function init(done) {
if (!RED.user.hasPermission("settings.write")) {
RED.notify(RED._("user.errors.settings"),"error");
return;
}
RED.userSettings.add({
id:'envvar',
title: RED._("env-var.environment"),

View File

@@ -35,6 +35,10 @@ RED.palette = (function() {
var categoryContainers = {};
var sidebarControls;
let paletteState = { filter: "", collapsed: [] };
let filterRefreshTimeout
function createCategory(originalCategory,rootCategory,category,ns) {
if ($("#red-ui-palette-base-category-"+rootCategory).length === 0) {
createCategoryContainer(originalCategory,rootCategory, ns+":palette.label."+rootCategory);
@@ -60,20 +64,57 @@ RED.palette = (function() {
catDiv.data('label',label);
categoryContainers[category] = {
container: catDiv,
close: function() {
hide: function (instant) {
if (instant) {
catDiv.hide()
} else {
catDiv.slideUp()
}
},
show: function () {
catDiv.show()
},
isOpen: function () {
return !!catDiv.hasClass("red-ui-palette-open")
},
getNodeCount: function (visibleOnly) {
const nodes = catDiv.find(".red-ui-palette-node")
if (visibleOnly) {
return nodes.filter(function() { return $(this).css('display') !== 'none'}).length
} else {
return nodes.length
}
},
close: function(instant, skipSaveState) {
catDiv.removeClass("red-ui-palette-open");
catDiv.addClass("red-ui-palette-closed");
$("#red-ui-palette-base-category-"+category).slideUp();
if (instant) {
$("#red-ui-palette-base-category-"+category).hide();
} else {
$("#red-ui-palette-base-category-"+category).slideUp();
}
$("#red-ui-palette-header-"+category+" i").removeClass("expanded");
if (!skipSaveState) {
if (!paletteState.collapsed.includes(category)) {
paletteState.collapsed.push(category);
savePaletteState();
}
}
},
open: function() {
open: function(skipSaveState) {
catDiv.addClass("red-ui-palette-open");
catDiv.removeClass("red-ui-palette-closed");
$("#red-ui-palette-base-category-"+category).slideDown();
$("#red-ui-palette-header-"+category+" i").addClass("expanded");
if (!skipSaveState) {
if (paletteState.collapsed.includes(category)) {
paletteState.collapsed.splice(paletteState.collapsed.indexOf(category), 1);
savePaletteState();
}
}
},
toggle: function() {
if (catDiv.hasClass("red-ui-palette-open")) {
if (categoryContainers[category].isOpen()) {
categoryContainers[category].close();
} else {
categoryContainers[category].open();
@@ -415,8 +456,16 @@ RED.palette = (function() {
var categoryNode = $("#red-ui-palette-container-"+rootCategory);
if (categoryNode.find(".red-ui-palette-node").length === 1) {
categoryContainers[rootCategory].open();
if (!paletteState?.collapsed?.includes(rootCategory)) {
categoryContainers[rootCategory].open();
} else {
categoryContainers[rootCategory].close(true);
}
}
clearTimeout(filterRefreshTimeout)
filterRefreshTimeout = setTimeout(() => {
refreshFilter()
}, 200)
}
}
@@ -516,7 +565,8 @@ RED.palette = (function() {
paletteNode.css("backgroundColor", sf.color);
}
function filterChange(val) {
function refreshFilter() {
const val = $("#red-ui-palette-search input").val()
var re = new RegExp(val.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),'i');
$("#red-ui-palette-container .red-ui-palette-node").each(function(i,el) {
var currentLabel = $(el).attr("data-palette-label");
@@ -528,16 +578,26 @@ RED.palette = (function() {
}
});
for (var category in categoryContainers) {
for (let category in categoryContainers) {
if (categoryContainers.hasOwnProperty(category)) {
if (categoryContainers[category].container
.find(".red-ui-palette-node")
.filter(function() { return $(this).css('display') !== 'none'}).length === 0) {
categoryContainers[category].close();
categoryContainers[category].container.slideUp();
const categorySection = categoryContainers[category]
if (categorySection.getNodeCount(true) === 0) {
categorySection.hide()
} else {
categoryContainers[category].open();
categoryContainers[category].container.show();
categorySection.show()
if (val) {
// There is a filter being applied and it has matched
// something in this category - show the contents
categorySection.open(true)
} else {
// No filter. Only show the category if it isn't in lastState
if (!paletteState.collapsed.includes(category)) {
categorySection.open(true)
} else if (categorySection.isOpen()) {
// This section should be collapsed but isn't - so make it so
categorySection.close(true, true)
}
}
}
}
}
@@ -553,6 +613,9 @@ RED.palette = (function() {
$("#red-ui-palette > .red-ui-palette-spinner").show();
RED.events.on('logout', function () {
RED.settings.removeLocal('palette-state')
})
RED.events.on('registry:node-type-added', function(nodeType) {
var def = RED.nodes.getType(nodeType);
@@ -596,14 +659,14 @@ RED.palette = (function() {
RED.events.on("subflows:change",refreshSubflow);
$("#red-ui-palette-search input").searchBox({
delay: 100,
change: function() {
filterChange($(this).val());
refreshFilter();
paletteState.filter = $(this).val();
savePaletteState();
}
})
});
sidebarControls = $('<div class="red-ui-sidebar-control-left"><i class="fa fa-chevron-left"></i></div>').appendTo($("#red-ui-palette"));
RED.popover.tooltip(sidebarControls,RED._("keyboard.togglePalette"),"core:toggle-palette");
@@ -669,7 +732,23 @@ RED.palette = (function() {
togglePalette(state);
}
});
try {
paletteState = JSON.parse(RED.settings.getLocal("palette-state") || '{"filter":"", "collapsed": []}');
if (paletteState.filter) {
// Apply the category filter
$("#red-ui-palette-search input").searchBox("value", paletteState.filter);
}
} catch (error) {
console.error("Unexpected error loading palette state from localStorage: ", error);
}
setTimeout(() => {
// Lazily tidy up any categories that haven't been reloaded
paletteState.collapsed = paletteState.collapsed.filter(category => !!categoryContainers[category])
savePaletteState()
}, 10000)
}
function togglePalette(state) {
if (!state) {
$("#red-ui-main-container").addClass("red-ui-palette-closed");
@@ -689,6 +768,15 @@ RED.palette = (function() {
})
return categories;
}
function savePaletteState() {
try {
RED.settings.setLocal("palette-state", JSON.stringify(paletteState));
} catch (error) {
console.error("Unexpected error saving palette state to localStorage: ", error);
}
}
return {
init: init,
add:addNodeType,

View File

@@ -187,6 +187,7 @@ RED.user = (function() {
}
function logout() {
RED.events.emit('logout')
var tokens = RED.settings.get("auth-tokens");
var token = tokens?tokens.access_token:"";
$.ajax({
@@ -225,6 +226,7 @@ RED.user = (function() {
});
}
});
$('<i class="fa fa-user"></i>').appendTo("#red-ui-header-button-user");
} else {
RED.menu.addItem("red-ui-header-button-user",{
id:"usermenu-item-username",
@@ -237,6 +239,15 @@ RED.user = (function() {
RED.user.logout();
}
});
const userMenu = $("#red-ui-header-button-user")
userMenu.empty()
if (RED.settings.user.image) {
$('<span class="user-profile"></span>').css({
backgroundImage: "url("+RED.settings.user.image+")",
}).appendTo(userMenu);
} else {
$('<i class="fa fa-user"></i>').appendTo(userMenu);
}
}
}
@@ -247,14 +258,6 @@ RED.user = (function() {
var userMenu = $('<li><a id="red-ui-header-button-user" class="button hide" href="#"></a></li>')
.prependTo(".red-ui-header-toolbar");
if (RED.settings.user.image) {
$('<span class="user-profile"></span>').css({
backgroundImage: "url("+RED.settings.user.image+")",
}).appendTo(userMenu.find("a"));
} else {
$('<i class="fa fa-user"></i>').appendTo(userMenu.find("a"));
}
RED.menu.init({id:"red-ui-header-button-user",
options: []
});

View File

@@ -63,25 +63,29 @@
}
.red-ui-header-toolbar {
display: flex;
align-items: stretch;
padding: 0;
margin: 0;
list-style: none;
float: right;
> li {
display: inline-block;
display: inline-flex;
align-items: stretch;
padding: 0;
margin: 0;
position: relative;
}
}
.button {
height: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
text-align: center;
line-height: 40px;
display: inline-block;
font-size: 20px;
padding: 0px 12px;
text-decoration: none;
@@ -271,13 +275,13 @@
color: var(--red-ui-header-menu-heading-color);
}
#red-ui-header-button-user .user-profile {
.user-profile {
background-position: center center;
background-repeat: no-repeat;
background-size: contain;
display: inline-block;
width: 40px;
height: 35px;
width: 30px;
height: 30px;
vertical-align: middle;
}
}

View File

@@ -17,7 +17,8 @@ export default {
{
title: {
"en-US": "Timestamp formatting options",
"ja": "タイムスタンプの形式の項目"
"ja": "タイムスタンプの形式の項目",
"fr": "Options de formatage de l'horodatage"
},
image: 'images/nr4-timestamp-formatting.png',
description: {
@@ -34,13 +35,21 @@ export default {
<li>エポックからのミリ秒 - 従来動作と同じになるタイムスタンプの項目</li>
<li>ISO 8601 - 多くのシステムで使用されている共通の形式</li>
<li>JavaScript日付オブジェクト</li>
</ul>`,
"fr": `<p>Les noeuds qui vous permettent de définir un horodatage disposent désormais d'options sur le format dans lequel cet horodatage peut être défini.</p>
<p>Nous gardons les choses simples en proposant trois options :<p>
<ul>
<li>Millisecondes depuis l'époque : il s'agit du comportement existant de l'option d'horodatage</li>
<li>ISO 8601 : un format commun utilisé par de nombreux systèmes</li>
<li>Objet Date JavaScript</li>
</ul>`
}
},
{
title: {
"en-US": "Auto-complete of flow/global and env types",
"ja": "フロー/グローバル、環境変数の型の自動補完"
"ja": "フロー/グローバル、環境変数の型の自動補完",
"fr": "Saisie automatique des types de flux/global et env"
},
image: 'images/nr4-auto-complete.png',
description: {
@@ -48,13 +57,17 @@ export default {
now all include auto-complete suggestions based on the live state of your flows.</p>
`,
"ja": `<p><code>flow</code>/<code>global</code>コンテキストや<code>env</code>の入力を、現在のフローの状態をもとに自動補完で提案するようになりました。</p>
`
`,
"fr": `<p>Les entrées contextuelles <code>flow</code>/<code>global</code> et l'entrée <code>env</code>
incluent désormais des suggestions de saisie semi-automatique basées sur l'état actuel de vos flux.</p>
`,
}
},
{
title: {
"en-US": "Config node customisation in Subflows",
"ja": "サブフローでの設定ノードのカスタマイズ"
"ja": "サブフローでの設定ノードのカスタマイズ",
"fr": "Personnalisation du noeud de configuration dans les sous-flux"
},
image: 'images/nr4-sf-config.png',
description: {
@@ -65,6 +78,11 @@ export default {
`,
"ja": `<p>サブフローをカスタマイズして、選択した型の異なる設定ノードを各インスタンスが使用できるようになりました。</p>
<p>例えば、MQTTブローカへ接続し、メッセージ受信と後処理を行うサブフローの各インスタンスに異なるブローカを指定することも可能です。</p>
`,
"fr": `<p>Les sous-flux peuvent désormais être personnalisés pour permettre à chaque instance d'utiliser un
noeud de configuration d'un type sélectionné.</p>
<p>Par exemple, chaque instance d'un sous-flux qui se connecte à un courtier MQTT et effectue un post-traitement
des messages reçus peut être pointée vers un autre courtier.</p>
`
}
},
@@ -90,6 +108,14 @@ export default {
<li>WebSocketードのカスタマイズ可能なヘッダ</li>
<li>Splitードは、メッセージプロパティで操作できるようになりました</li>
<li>他にも沢山あります...</li>
</ul>`,
"fr": `<p>Les noeuds principaux ont reçu de nombreux correctifs mineurs ainsi que des améliorations. La documentation a été mise à jour.
Consultez le journal des modifications dans la barre latérale d'aide pour une liste complète. Ci-dessous, les changements les plus importants :</p>
<ul>
<li>Un mode CSV entièrement conforme à la norme RFC4180</li>
<li>En-têtes personnalisables pour le noeud WebSocket</li>
<li>Le noeud Split peut désormais fonctionner sur n'importe quelle propriété de message</li>
<li>Et bien plus encore...</li>
</ul>`
}
}

View File

@@ -0,0 +1,3 @@
<script type="text/html" data-help-name="global-config">
<p>Un noeud pour contenir la configuration globale des flux.</p>
</script>

View File

@@ -94,6 +94,7 @@
},
"catch": {
"catch": "catch : tout",
"catchGroup": "catch: groupe",
"catchNodes": "catch : __number__",
"catchUncaught": "catch : non capturé",
"label": {
@@ -109,6 +110,7 @@
},
"status": {
"status": "statut : tout",
"statusGroup": "statut: groupe",
"statusNodes": "statut : __number__",
"label": {
"source": "Signaler l'état de",
@@ -250,7 +252,8 @@
"initialize": "Au démarrage",
"finalize": "À l'arrêt",
"outputs": "Sorties",
"modules": "Modules"
"modules": "Modules",
"timeout": "Délai d'attente"
},
"text": {
"initialize": "// Le code ajouté ici sera exécuté une fois\n// à chaque démarrage du noeud.\n",
@@ -847,7 +850,13 @@
"newline": "Nouvelle ligne",
"usestrings": "Analyser les valeurs numériques",
"include_empty_strings": "Inclure les chaînes vides",
"include_null_values": "Inclure les valeurs nulles"
"include_null_values": "Inclure les valeurs nulles",
"spec": "Analyseur"
},
"spec": {
"rfc": "RFC4180",
"legacy": "Hérité (Legacy)",
"legacy_warning": "Le mode hérité sera supprimé dans une prochaine version."
},
"placeholder": {
"columns": "noms de colonnes séparés par des virgules"
@@ -876,6 +885,7 @@
"once": "envoyer les en-têtes une fois, jusqu'à msg.reset"
},
"errors": {
"bad_template": "Colonnes du modèle mal formées.",
"csv_js": "Ce noeud ne gère que les chaînes CSV ou les objets js.",
"obj_csv": "Aucun modèle de colonnes spécifié pour l'objet -> CSV.",
"bad_csv": "Données CSV mal formées - sortie probablement corrompue."
@@ -885,12 +895,14 @@
"label": {
"select": "Sélecteur",
"output": "Sortie",
"in": "dans"
"in": "dans",
"prefix": "Nom de la propriété pour le contenu HTML"
},
"output": {
"html": "le contenu html des éléments",
"text": "uniquement le contenu textuel des éléments",
"attr": "un objet de n'importe quel attribut des éléments"
"attr": "un objet de n'importe quel attribut des éléments",
"compl": "un objet pour tous les attributs de tous les éléments ainsi que du contenu HTML"
},
"format": {
"single": "comme un seul message contenant un tableau",

View File

@@ -30,6 +30,8 @@
avant d'être envoyé.</p>
<p>Si <code>msg._session</code> n'est pas présent, la charge utile est
envoyé à <b>tous</b> les clients connectés.</p>
<p>En mode Répondre à, définir <code>msg.reset = true</code> réinitialisera la connexion
spécifiée par _session.id ou toutes les connexions si aucun _session.id n'est spécifié.</p>
<p><b>Remarque</b> : Sur certains systèmes, vous aurez peut-être besoin d'un accès root ou administrateur
pour accéder aux ports inférieurs à 1024.</p>
</script>
@@ -40,6 +42,8 @@
caractères renvoyés dans un tampon fixe, correspondant à un caractère spécifié avant de revenir,
attendre un délai fixe à partir de la première réponse, puis revenir, s'installer et attender les données, ou envoie puis ferme la connexion
immédiatement, sans attendre de réponse.</p>
<p>Dans le cas du mode veille (maintien de la connexion), vous pouvez envoyer <code>msg.reset = true</code> ou <code>msg.reset = "host:port"</code> pour forcer une interruption
de la connexion et une reconnexion automatique.</p>
<p>La réponse sortira dans <code>msg.payload</code> en tant que tampon, vous pouvez alors utiliser la fonction .toString().</p>
<p>Si vous laissez l'hôte ou le port tcp vide, ils doivent être définis à l'aide des propriétés <code>msg.host</code> et <code>msg.port</code> dans chaque message envoyé au noeud.</ p>
</script>

View File

@@ -36,7 +36,9 @@
</dl>
<h3>Détails</h3>
<p>Le modèle de colonne peut contenir une liste ordonnée de noms de colonnes. Lors de la conversion de CSV en objet, les noms de colonne
seront utilisés comme noms de propriété. Alternativement, les noms de colonne peuvent être tirés de la première ligne du CSV.</p>
seront utilisés comme noms de propriété. Alternativement, les noms de colonne peuvent être tirés de la première ligne du CSV.
<p>Lorsque l'analyseur RFC est sélectionné, le modèle de colonne doit être conforme à la norme RFC4180.</p>
</p>
<p>Lors de la conversion au format CSV, le modèle de colonnes est utilisé pour identifier les propriétés à extraire de l'objet et dans quel ordre.</p>
<p>Si le modèle de colonnes est vide, vous pouvez utiliser une simple liste de propriétés séparées par des virgules fournies dans <code>msg.columns</code> pour
déterminer quoi extraire et dans quel ordre. Si ni l'un ni l'autre n'est présent, toutes les propriétés de l'objet sont sorties dans l'ordre

View File

@@ -36,7 +36,7 @@ var connections = [];
const events = require("@node-red/util").events;
function handleCommsEvent(event) {
publish(event.topic,event.data,event.retain);
publish(event.topic,event.data,event.retain,event.session,event.excludeSession);
}
function handleStatusEvent(event) {
if (!event.status) {
@@ -74,13 +74,17 @@ function handleEventLog(event) {
publish("event-log/"+event.id,event.payload||{});
}
function publish(topic,data,retain) {
function publish(topic, data, retain, session, excludeSession) {
if (retain) {
retained[topic] = data;
} else {
delete retained[topic];
}
connections.forEach(connection => connection.send(topic,data))
connections.forEach(connection => {
if ((!session || connection.session === session) && (!excludeSession || connection.session !== excludeSession)) {
connection.send(topic,data)
}
})
}
@@ -109,6 +113,10 @@ var api = module.exports = {
*/
addConnection: async function(opts) {
connections.push(opts.client);
events.emit('comms:connection-added', {
session: opts.client.session,
user: opts.client.user
})
},
/**
@@ -126,6 +134,9 @@ var api = module.exports = {
break;
}
}
events.emit('comms:connection-removed', {
session: opts.client.session
})
},
/**
@@ -157,5 +168,23 @@ var api = module.exports = {
* @return {Promise<Object>} - resolves when complete
* @memberof @node-red/runtime_comms
*/
unsubscribe: async function(opts) {}
unsubscribe: async function(opts) {},
/**
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {CommsConnection} opts.client - the client connection
* @param {String} opts.topic - the message topic
* @param {String} opts.data - the message data
* @return {Promise<Object>} - resolves when complete
*/
receive: async function (opts) {
if (opts.topic) {
events.emit('comms:message:' + opts.topic, {
session: opts.client.session,
user: opts.user,
data: opts.data
})
}
}
};