node-red-contrib-datacake-helpers
Version:
Node-RED nodes to interact with the Datacake GraphQL API for KPIs and device stats. Includes other helper nodes for device management.
302 lines (272 loc) • 13.1 kB
HTML
<script type="text/javascript">
RED.nodes.registerType('datacakegraphql-product-stats', {
category: 'Datacake',
color: '#3FADB5',
defaults: {
name: { value: "" },
datacakeConfig: { value: "", type: "datacakegraphql-config", required: true },
productId: { value: "", required: true },
selectedFields: { value: [] },
availableProducts: { value: [] },
availableFields: { value: [] },
searchTags: { value: [] },
searchTagsAnyAll: { value: "any" },
availableTags: { value: [] }
},
credentials: {},
inputs: 1,
outputs: 1,
icon: "icons/datacake.svg",
label: function() {
return this.name || "Datacake Product Stats";
},
paletteLabel: "Product Stats",
oneditprepare: function() {
const node = this;
const productSelect = $('#node-input-productId');
const fieldsContainer = $('#selected-fields-container');
const tagsContainer = $('#selected-tags-container');
const anyAllSelect = $('#node-input-searchTagsAnyAll');
// Set selected tags matching logic
if (node.searchTagsAnyAll) {
anyAllSelect.val(node.searchTagsAnyAll);
}
// Function to populate the product select dropdown
function populateProducts(products) {
productSelect.empty();
productSelect.append('<option value="">Select a product</option>');
if (products && products.data && products.data.allProducts) {
node.availableProducts = products.data.allProducts;
products.data.allProducts.forEach(product => {
productSelect.append(`<option value="${product.id}">${product.name}</option>`);
});
// Set previously selected product
if (node.productId) {
productSelect.val(node.productId);
// Populate fields for the selected product
populateFields(node.productId);
}
}
}
// Function to populate available tags
function populateTags(tagsData) {
tagsContainer.empty();
if (tagsData && tagsData.data && tagsData.data.workspace && tagsData.data.workspace.allTags) {
node.availableTags = tagsData.data.workspace.allTags;
if (node.availableTags.length === 0) {
tagsContainer.append('<p>No tags available in this workspace.</p>');
return;
}
// Create checkboxes for each tag
let tagsHTML = '<div class="form-row tags-list">';
node.availableTags.forEach(tag => {
const isSelected = node.searchTags &&
node.searchTags.includes(tag);
tagsHTML += `
<div class="form-row tag-row">
<label>
<input type="checkbox" class="tag-checkbox"
data-tag="${tag}"
${isSelected ? 'checked' : ''}>
${tag}
</label>
</div>
`;
});
tagsHTML += '</div>';
tagsContainer.append(tagsHTML);
} else {
tagsContainer.append('<p>No tags found or error fetching tags.</p>');
}
}
// Find product by ID
function findProductById(productId) {
if (!node.availableProducts) return null;
return node.availableProducts.find(p => p.id === productId);
}
// Function to populate fields for the selected product
function populateFields(productId) {
const product = findProductById(productId);
if (!product || !product.measurementFields) return;
node.availableFields = product.measurementFields;
// Clear current fields display
fieldsContainer.empty();
if (product.measurementFields.length === 0) {
fieldsContainer.append('<p>No measurement fields available for this product.</p>');
return;
}
// Create checkboxes for each field
let fieldsHTML = '<div class="form-row fields-list">';
product.measurementFields.forEach(field => {
const isSelected = node.selectedFields &&
node.selectedFields.some(f => f.id === field.id);
fieldsHTML += `
<div class="form-row field-row">
<label>
<input type="checkbox" class="field-checkbox"
data-field-id="${field.id}"
data-field-name="${field.fieldName}"
data-field-type="${field.fieldType}"
${isSelected ? 'checked' : ''}>
${field.fieldName} (${field.fieldType})
</label>
</div>
`;
});
fieldsHTML += '</div>';
fieldsContainer.append(fieldsHTML);
}
// Handle config node selection change
$('#node-input-datacakeConfig').on('change', function() {
const configNodeId = $(this).val();
if (configNodeId) {
$('#product-loading').show();
$('#tags-loading').show();
// Fetch products from the selected config
$.getJSON('datacakegraphql/products', { configId: configNodeId })
.done(function(products) {
populateProducts(products);
$('#product-loading').hide();
})
.fail(function(err) {
console.error('Error fetching products:', err);
RED.notify(`Error fetching products: ${err.responseJSON ? err.responseJSON.error : err.statusText}`, "error");
$('#product-loading').hide();
});
// Fetch tags from the selected config
$.getJSON('datacakegraphql/tags', { configId: configNodeId })
.done(function(tags) {
populateTags(tags);
$('#tags-loading').hide();
})
.fail(function(err) {
console.error('Error fetching tags:', err);
RED.notify(`Error fetching tags: ${err.responseJSON ? err.responseJSON.error : err.statusText}`, "error");
$('#tags-loading').hide();
});
} else {
productSelect.empty();
productSelect.append('<option value="">Select a config first</option>');
fieldsContainer.empty();
tagsContainer.empty();
}
});
// Handle product selection change
productSelect.on('change', function() {
const productId = $(this).val();
if (productId) {
populateFields(productId);
} else {
fieldsContainer.empty();
}
});
// Trigger config node change if already selected
if (this.datacakeConfig) {
$('#node-input-datacakeConfig').trigger('change');
}
},
oneditsave: function() {
this.productId = $('#node-input-productId').val();
this.searchTagsAnyAll = $('#node-input-searchTagsAnyAll').val();
// Get selected fields
const selectedFields = [];
$('.field-checkbox:checked').each(function() {
const $checkbox = $(this);
selectedFields.push({
id: $checkbox.data('field-id'),
fieldName: $checkbox.data('field-name'),
fieldType: $checkbox.data('field-type')
});
});
this.selectedFields = selectedFields;
// Get selected tags
const selectedTags = [];
$('.tag-checkbox:checked').each(function() {
const $checkbox = $(this);
selectedTags.push($checkbox.data('tag'));
});
this.searchTags = selectedTags;
}
});
</script>
<script type="text/html" data-template-name="datacakegraphql-product-stats">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-datacakeConfig"><i class="fa fa-cog"></i> Config</label>
<input type="text" id="node-input-datacakeConfig">
</div>
<div class="form-row">
<label for="node-input-productId"><i class="fa fa-cubes"></i> Product</label>
<select type="text" id="node-input-productId">
<option value="">Select a config first</option>
</select>
<span id="product-loading" style="margin-left: 10px; display: none;">
<i class="fa fa-spinner fa-spin"></i> Loading products...
</span>
</div>
<div class="form-row">
<label><i class="fa fa-list"></i> Fields</label>
<div id="selected-fields-container" class="form-tips form-tips-auto-width">
<p>Select a product to view available fields</p>
</div>
</div>
<div class="form-row">
<label for="node-input-searchTagsAnyAll"><i class="fa fa-filter"></i> Tags Match</label>
<select id="node-input-searchTagsAnyAll">
<option value="any">Match ANY tag</option>
<option value="all">Match ALL tags</option>
</select>
</div>
<div class="form-row">
<label><i class="fa fa-tags"></i> Filter Tags</label>
<div id="selected-tags-container" class="form-tips form-tips-auto-width">
<p>Select a config to view available tags</p>
</div>
<span id="tags-loading" style="margin-left: 10px; display: none;">
<i class="fa fa-spinner fa-spin"></i> Loading tags...
</span>
</div>
</script>
<script type="text/html" data-help-name="datacakegraphql-product-stats">
<p>Node that calculates statistics for selected fields across all devices in a Datacake product.</p>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>payload <span class="property-type">any</span></dt>
<dd>Any input message will trigger a query to fetch the latest device data and calculate statistics.</dd>
</dl>
<h3>Outputs</h3>
<dl class="message-properties">
<dt>payload <span class="property-type">object</span></dt>
<dd>An object containing statistics for each selected field.</dd>
</dl>
<h3>Details</h3>
<p>This node calculates min, max, and average values for selected fields across all devices in a Datacake product.</p>
<p>You can filter devices by tags to include only devices that have specific tags.</p>
<p>For each field, the node provides:</p>
<ul>
<li>Minimum value and the device with that value</li>
<li>Maximum value and the device with that value</li>
<li>Average value across all devices</li>
<li>Count of devices with valid values for the field</li>
</ul>
<p>Example output:</p>
<pre>{
"SOIL_MOISTURE": {
"min": 23,
"max": 51,
"avg": 37,
"count": 2,
"minDevice": {
"id": "c9617e90-296d-47a8-900e-27cb68da59e9",
"name": "SFM Satinksplas"
},
"maxDevice": {
"id": "1456574d-b4c9-47fc-bcc5-03cb763c6b33",
"name": "SFM GKC"
}
}
}</pre>
</script>