Grafana Business Text HTML duplication?

Hello everyone

I tried to create a Kanban board in Grafana using the Business Text plugin. The board has 3 lanes, “ToDo”, “Progress”, “Done”. When you switch a task to a different lane, the status code in our table changes to 1, 2, or 3. However, if you now switch lanes, change the status code, and press the Grafana refresh button, Grafana will create the same task item in the same track. However, no second element is created in the database. My code seems to be correct and I don’t know how to fix this. Is it a Grafana special logic or something?

Thank you for any help :slight_smile:

See the pictures.


Please share your code

I changed the URLs and table-names in the API

// Variables for Facility and Workshop
let facility = context.grafana.replaceVariables('${facility}');
let workshopID = context.grafana.replaceVariables('${workshop}');

// Function to fetch tasks
async function fetchTasks(facility, workshopID) {
  const compositeKey = `${facility}.${workshopID}`;
  console.log(`Querying for: ${compositeKey}`);

  try {
    const response = await fetch("https://community.grafana.com/", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        stmt: `SELECT workshop, task, statuscode FROM i55.board WHERE workshop LIKE '${compositeKey}%'`
      })
    });

    if (!response.ok) {
      console.error("Error in API request:", response.statusText);
      return;
    }

    const data = await response.json();
    console.log('API response:', data);

    // Clear the Kanban zones before adding new tasks
    clearKanbanZones();

    if (data && Array.isArray(data.rows) && data.rows.length > 0) {
      // Add new tasks to the respective Kanban zones
      data.rows.forEach(([workshop, task, statuscode]) => {
        const taskElement = createTaskElement(task, statuscode, workshop); // Create task
        taskElement.setAttribute('data-task-id', workshop); // Add unique ID

        // Check if the task already exists in the DOM
        console.log(`Task ID: ${workshop}}`);
        const existingTask = document.querySelector(`[data-task-id="${workshop}"]`);

        if (!existingTask) {
          // Add task to the corresponding Kanban zone
          if (statuscode === 1) {
            document.querySelector('#todo .kanban-tasks').appendChild(taskElement);
          } else if (statuscode === 2) {
            document.querySelector('#inprogress .kanban-tasks').appendChild(taskElement);
          } else if (statuscode === 3) {
            document.querySelector('#done .kanban-tasks').appendChild(taskElement);
          }
        }
      });

      console.log(`Number of tasks found: ${data.rowcount}`);
      console.log(`Query duration: ${data.duration} ms`);
    } else {
      console.log("No tasks found or unexpected format:", data);
      // If no tasks were found, clear the area
      clearKanbanZones();
    }
  } catch (error) {
    console.error("Error while fetching tasks:", error);
    // If an error occurs, clear the area
    clearKanbanZones();
  }
}

// Function to clear the Kanban zones
function clearKanbanZones() {
  const zones = document.querySelectorAll('.kanban-tasks');
  zones.forEach(zone => {
    zone.innerHTML = ''; // Clear the contents of the Kanban zones
    console.log("Tasks cleared");
  });
}

// Function to create a task with drag-and-drop
function createTaskElement(taskData, statuscode, workshop) {
  const { description, title, priority, category } = taskData;

  // Create a new task element
  const newTask = document.createElement("div");
  const priorityClass = getPriorityClass(priority);
  console.log("Creating new task element");
  newTask.className = `task ${priorityClass}`;
  newTask.setAttribute("draggable", "true");
  newTask.setAttribute("data-statuscode", statuscode);  // Add status code as attribute
  newTask.setAttribute("data-task-id", workshop); // Add unique ID
  newTask.innerHTML = `<p draggable="true"> <strong>${title}</strong> <br> 
                       ${description} <br>
                       Category: ${category}</p>`;

  // Add drag events for the new task
  newTask.addEventListener("dragstart", (e) => {
    newTask.classList.add("is-dragging");
    e.dataTransfer.setData("text/plain", newTask.innerHTML);  // Set the task in the DataTransfer object
    e.dataTransfer.setData("statuscode", statuscode); // Set status code in DataTransfer
  });

  newTask.addEventListener("dragend", () => {
    newTask.classList.remove("is-dragging");
  });
  return newTask;
}

// Drag-and-drop functionality for existing tasks
const draggables = document.querySelectorAll(".task");
const droppables = document.querySelectorAll(".kanban-block");

// Add drag events for all draggable tasks
draggables.forEach((task) => {
  task.addEventListener("dragstart", () => {
    task.classList.add("is-dragging");
  });

  task.addEventListener("dragend", () => {
    task.classList.remove("is-dragging");
  });
});

// Add event listeners to all droppable zones
droppables.forEach((zone) => {
  zone.addEventListener("dragover", (e) => {
    e.preventDefault();
    zone.classList.add("drag-over");
    const bottomTask = insertAboveTask(zone, e.clientY);
    const curTask = document.querySelector(".is-dragging");

    if (!bottomTask) {
      zone.appendChild(curTask);
    } else {
      zone.insertBefore(curTask, bottomTask);
    }
  });

  zone.addEventListener("dragleave", () => {
    zone.classList.remove("drag-over");
  });

  zone.addEventListener("drop", () => {
    zone.classList.remove("drag-over");

    // Determine the task title
    const task = document.querySelector(".is-dragging");
    const title = task.querySelector("p").innerText.split(" - ")[0]; // Extract the task title

    // Set the workshop value based on the variables
    const workshop = context.grafana.replaceVariables('${facility}') + "." +
      context.grafana.replaceVariables('${workshop}') + "." +
      title;

    // Determine the status based on the zone
    const status = getStatusFromZone(zone.id); // Get status from zone ID
    updateTaskStatus(workshop, status); // API call to update the status
  });
});

// Helper function to determine status based on zone
function getStatusFromZone(zoneId) {
  switch (zoneId) {
    case 'inprogress':
      return 2; // In Progress
    case 'done':
      return 3; // Done
    case 'todo':
      return 1; // To Do (default status)
    default:
      return 1; // Default to To Do if unknown
  }
}

// Function to determine the closest task in a droppable zone
function insertAboveTask(zone, mouseY) {
  const tasks = zone.querySelectorAll('.task:not(.is-dragging)');
  let closestTask = null;
  let closestOffset = Number.NEGATIVE_INFINITY;

  tasks.forEach(task => {
    const box = task.getBoundingClientRect();
    const offset = mouseY - box.top;

    if (offset >= 0 && offset < box.height) {
      if (closestOffset === Number.NEGATIVE_INFINITY || offset < closestOffset) {
        closestOffset = offset;
        closestTask = task;
      }
    }
  });

  return closestTask;
}

// API call to update status
const updateTaskStatus = (workshop, status) => {
  fetch(`https://community.grafana.com/`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      stmt: 'UPDATE i55.board SET statuscode = ? WHERE workshop = ?',
      args: [status, workshop] // Use 'workshop' as primary key
    })
  })
    .then(response => response.json())
    .then(result => console.log('Status updated:', result))
    .catch(error => console.error('Error:', error));
};

// Open and close modal
const taskButton = document.getElementById('task-button');
const modal = document.getElementById('todo-modal');
const closeModal = modal.querySelector('.close');
const todoForm = document.getElementById('todo-form');

taskButton.onclick = function (event) {
  modal.style.display = "block";
};

closeModal.onclick = function () {
  modal.style.display = "none";
};

window.onclick = function (event) {
  if (event.target === modal) {
    modal.style.display = "none";
  }
};

// Function to determine the priority class
function getPriorityClass(priority) {
  switch (priority.toLowerCase()) {
    case 'hoch':
      return 'high-priority';
    case 'mittel':
      return 'medium-priority';
    case 'niedrig':
      return 'low-priority';
    default:
      return ''; // No priority
  }
}

// Create task on form submission
todoForm.onsubmit = function (event) {
  event.preventDefault(); // Prevent default form submission

  const title = document.getElementById("title").value;
  const description = document.getElementById("description").value;
  const priority = document.getElementById("priority").value;
  const category = document.getElementById("category").value;
  let statuscode = 1;
  let workshop = context.grafana.replaceVariables('${facility}') + "." +
    context.grafana.replaceVariables('${workshop}') + "." +
    title;

  const task = {
    title: title,
    description: description,
    priority: priority,
    category: category
  };

  if (!title) return; // Cancel if the title is empty

  const taskElement = createTaskElement(task, statuscode, workshop); // Create task
  document.querySelector('#todo .kanban-tasks').appendChild(taskElement);

  // Save task using API
  saveTaskToDatabase(workshop, task, statuscode);

  modal.style.display = "none"; // Close modal after submission
};

// API call to save the task to the database
const saveTaskToDatabase = (workshop, task, statuscode) => {
  fetch('https://community.grafana.com/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      stmt: 'INSERT INTO i55.board (workshop, task, statuscode) VALUES (?, ?, ?)',
      args: [workshop, task.title, statuscode]
    })
  })
    .then(response => response.json())
    .then(result => console.log('Task saved:', result))
    .catch(error => console.error('Error saving task:', error));
};`Preformatted text`
1 Like

Definitely have try catch on saveTaskToDatabase and updateTaskStatus and see what errors there might be also check console in browser and network tab in browser something must be failing on the save and/or update

1 Like

Thank you, I will try it

@pascalstahle1 That looks very cool. Impressive what you did with Business Text panel.

Let me know if you interested to share use case and details how you implemented it in the blog post on volkovlabs.io to share with the Community.