Two separate panels using REST API to post payload to two different tables

What Grafana version and what operating system are you using?
Grafana v11.3, Windows 11

What are you trying to achieve?

I am trying to configure two separate panels in Grafana that send data to different tables in a backend server. One panel is for updating user inputs (kWh rate and target usage), and the other panel is for setting thresholds (e.g., maximum voltage, voltage imbalance, and current limits). The tables are named ‘user_inputs’ and ‘thresholds’ accordingly.

How are you trying to achieve it?

I have implemented custom server-side code to handle incoming POST requests from each panel and map them to the respective tables. The POST request structure and server routes are defined separately for /user_inputs and /thresholds. Each panel is configured to send JSON payloads containing its respective values.

What happened?

  • POST requests are being received by the server, but values are not updating correctly for both panels. The user_inputs route logs "Updated values successfully but displays fallback values of ‘Admin’, 0, 0 and not the actual values I set.
  • No matching route errors are intermittently logged.
  • Values sent in the payload appear as null or default values in the server logs, despite being set in the panel fields.

What did you expect to happen?

  • The /user_inputs panel should send user_id ,kwh_rate and target_usage values to the user_inputs table.
  • The /thresholds panel should send various threshold parameters (e.g., max_v, voltage_imbalance, etc.) to the thresholds table.
  • Each panel should display a notification confirming the updated values.
  • Data should be correctly logged and stored without null values or mismatched routes.

Can you copy/paste the configuration(s) that you are having problems with?

JS Server Code

const http = require(‘http’);
const { Client } = require(‘pg’);
require(‘dotenv’).config();

const client = new Client({
user: process.env.POSTGRES_USER,
host: process.env.POSTGRES_HOST,
database: process.env.POSTGRES_DB,
password: process.env.POSTGRES_PASSWORD,
port: process.env.POSTGRES_PORT
});

client.connect();

const server = http.createServer((req, res) => {
console.log(Received request: ${req.method} ${req.url});

res.setHeader(‘Access-Control-Allow-Origin’, ‘*’);
res.setHeader(‘Access-Control-Allow-Methods’, ‘OPTIONS, POST, PUT’);
res.setHeader(‘Access-Control-Allow-Headers’, ‘Content-Type’);

if (req.method === ‘OPTIONS’) {
res.writeHead(200);
res.end();
return;
}

if (req.method === ‘POST’ && req.url === ‘/user_inputs’) {
console.log(‘POST request received for user_inputs’);

let body = '';

req.on('data', (chunk) => {
  body += chunk;
});

req.on('end', async () => {
  try {
    const values = JSON.parse(body);
    console.log('Received data for user_inputs:', values);

    // Validate required fields
    const requiredFields = ['user_id', 'kwh_rate', 'target_usage'];
    const missingFields = requiredFields.filter(
      (field) => !Object.hasOwn(values, field) || values[field] === null
    );

    if (missingFields.length > 0) {
      res.writeHead(400, { 'Content-Type': 'application/json' });
      res.end(
        JSON.stringify({
          message: `Missing required fields: ${missingFields.join(', ')}`,
        })
      );
      return;
    }

    const columns = Object.keys(values).join(', ');
    const params = Object.values(values);
    const placeholders = Object.keys(values)
      .map((_, i) => `$${i + 1}`)
      .join(', ');

    const query = `INSERT INTO user_inputs(${columns}) VALUES(${placeholders})`;

    await client.query(query, params);

    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(
      JSON.stringify({ message: 'Data inserted successfully into user_inputs' })
    );
  } catch (err) {
    console.error('Error processing user_inputs:', err);
    res.writeHead(500, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ message: 'Error inserting data into user_inputs' }));
  }
});

return;

}

if (req.method === ‘POST’ && req.url === ‘/thresholds’) {
console.log(‘POST request received for thresholds’);

let body = '';

req.on('data', (chunk) => {
  body += chunk;
});

req.on('end', async () => {
  try {
    const values = JSON.parse(body);
    console.log('Received data for thresholds:', values);

    // Validate required fields
    const requiredFields = [
      'meter_id',
      'max_v',
      'voltage_imbalance',
      'warning_current',
      'trip_off_current',
      'kVAR_max',
      'kVAR_imbalance',
      'kW_max',
      'kW_imbalance',
      'pf_max',
      'vTHD_limit',
      'iTHD_limit',
    ];
    const missingFields = requiredFields.filter(
      (field) => !Object.hasOwn(values, field) || values[field] === null
    );

    if (missingFields.length > 0) {
      res.writeHead(400, { 'Content-Type': 'application/json' });
      res.end(
        JSON.stringify({
          message: `Missing required fields: ${missingFields.join(', ')}`,
        })
      );
      return;
    }

    const columns = Object.keys(values).join(', ');
    const params = Object.values(values);
    const placeholders = Object.keys(values)
      .map((_, i) => `$${i + 1}`)
      .join(', ');

    const query = `INSERT INTO thresholds(${columns}) VALUES(${placeholders})`;

    await client.query(query, params);

    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(
      JSON.stringify({ message: 'Data inserted successfully into thresholds' })
    );
  } catch (err) {
    console.error('Error processing thresholds:', err);
    res.writeHead(500, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ message: 'Error inserting data into thresholds' }));
  }
});

return;

}

console.log(‘No matching route found’);
res.writeHead(404, { ‘Content-Type’: ‘application/json’ });
res.end(JSON.stringify({ message: ‘Not Found’ }));
});

server.listen(3001, () => {
console.log(‘Server for Postgres is running on port 3001…’);
});

This is Panel 1 for User Inputs

Custom Code

//const table = “user_inputs”; // Specify the table name explicitly

// Safely access user_id
const user_id = document.getElementById(‘user_id’) || ‘Admin’;;
const kwh_rate = parseFloat(document.getElementById(‘kwh_rate’)?.value || 0);
const target_usage = parseInt(document.getElementById(‘target_usage’)?.value || 0);

// Prepare the data payload
const data = {
//table, // Specify the target table for the API
user_id,
kwh_rate,
target_usage
};

fetch(‘http://localhost:3001/user_inputs’, {
method: ‘POST’,
headers: {
‘Content-Type’: ‘application/json’,
},
body: JSON.stringify(data),
})
.then(response => {
if (response.ok) {
context.grafana.notifySuccess([‘Update’, ‘Values updated successfully.’]);
context.grafana.locationService.reload();
} else {
context.grafana.notifyError([‘Update’, ‘An error occurred updating values.’]);
}
})
.catch(error => {
context.grafana.notifyError([‘Update’, 'Error occurred: ’ + error.message]);
});

This is Panel 2

Custom Code

//const table = “thresholds”;
// Constants derived from the provided configuration
const max_v = parseFloat(document.getElementById(‘max_v’)?.value);
const voltage_imbalance = parseFloat(document.getElementById(‘voltage_imbalance’)?.value);
const warning_current = parseFloat(document.getElementById(‘warning_current’)?.value);
const trip_off_current = parseFloat(document.getElementById(‘trip_off_current’)?.value);
const kVAR_max = parseFloat(document.getElementById(‘kVAR_max’)?.value);
const kVAR_imbalance = parseFloat(document.getElementById(‘kVAR_imbalance’)?.value);
const kW_max = parseFloat(document.getElementById(‘kW_max’)?.value);
const kW_imbalance = parseFloat(document.getElementById(‘kW_imbalance’)?.value);
const pf_max = parseFloat(document.getElementById(‘pf_max’)?.value);
const vTHD_limit = parseFloat(document.getElementById(‘vTHD_limit’)?.value);
const iTHD_limit = parseFloat(document.getElementById(‘iTHD_limit’)?.value);

// Prepare the data payload
const data = {
//table: “thresholds”, // Specify the target table for the API
max_v,
voltage_imbalance,
warning_current,
trip_off_current,
kVAR_max,
kVAR_imbalance,
kW_max,
kW_imbalance,
pf_max,
vTHD_limit,
iTHD_limit
};

// Send the data to the API
fetch(‘http://localhost:3001/thresholds’, {
method: ‘POST’,
headers: {
‘Content-Type’: ‘application/json’,
},
body: JSON.stringify(data),
})
.then(response => {
if (response.ok) {
context.grafana.notifySuccess([‘Update’, ‘Values updated successfully.’]);
context.grafana.locationService.reload();
} else {
context.grafana.notifyError([‘Update’, ‘An error occurred updating values.’]);
}
})
.catch(error => {
context.grafana.notifyError([‘Update’, 'Error occurred: ’ + error.message]);
});

Did you receive any errors in the Grafana UI or in related logs? If so, please tell us exactly what they were.

  • “No matching route found” for some POST requests. I think this makes sense for the root route since I have no request routed there?
  • Received data logs showing null values for payload fields.

Did you follow any online instructions? If so, what is the URL?
N/A – this issue is related to custom integrations not fully covered by existing documentation.

hello, this has been resolved. I figured the solution to this. There’s no need for a fetch clause in the custom code. i just modified the ‘url to call’ as the url + the route on grafana’s business forms plugin panel configuration.

1 Like