node-red-contrib-code-analyzer
Version:
A Node-RED package that provides a background service to detect debugging artifacts in function nodes across Node-RED flows. Features performance monitoring (CPU, memory, event loop), queue monitoring, and Slack alerting.
883 lines (781 loc) • 41.7 kB
JavaScript
// Dashboard JavaScript
/* global Chart */
class QualityDashboard {
constructor() {
this.charts = {};
this.refreshInterval = null;
this.currentTimeframe = 24; // hours
this.init();
}
async init() {
this.setupEventListeners();
await this.loadDashboardData();
this.startAutoRefresh();
}
setupEventListeners() {
// Refresh button
document.getElementById('refreshBtn').addEventListener('click', () => {
this.loadDashboardData();
});
// Trend timeframe buttons
document.querySelectorAll('.trend-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const hours = parseInt(e.target.dataset.hours);
this.updateTimeframe(hours);
});
});
// Performance metric selector
document.getElementById('performanceMetricSelect').addEventListener('change', (e) => {
this.updatePerformanceChart(e.target.value);
});
}
async loadDashboardData() {
try {
this.showLoading(true);
const [summary, qualityTrends, problematicNodes, alerts] = await Promise.all([
this.fetchAPI('/code-analyzer/api/dashboard/summary'),
this.fetchAPI(`/code-analyzer/api/dashboard/quality-trends?hours=${this.currentTimeframe}`),
this.fetchAPI('/code-analyzer/api/dashboard/problematic-nodes?limit=10'),
this.fetchAPI('/code-analyzer/api/dashboard/alerts?limit=20')
]);
this.updateOverviewCards(summary);
this.updateQualityTrendChart(qualityTrends);
this.updateFlowsList(summary.quality.flows);
this.updateProblematicNodesList(problematicNodes.nodes);
this.updateAlertsList(alerts.alerts);
// Load performance chart
await this.updatePerformanceChart('cpu');
this.updateLastUpdated(summary.timestamp);
this.showLoading(false);
} catch (error) {
this.showError('Failed to load dashboard data. Please check your connection.');
this.showLoading(false);
}
}
async fetchAPI(endpoint) {
const response = await fetch(endpoint);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
updateOverviewCards(data) {
const { quality } = data;
const { systemTrends, overallGrade, summary } = quality;
// Overall Quality
document.getElementById('overallQuality').textContent = `${systemTrends.overallQuality}%`;
const gradeElement = document.getElementById('qualityGrade');
const gradeText = document.getElementById('gradeText');
gradeElement.style.backgroundColor = overallGrade.color + '20';
gradeElement.style.color = overallGrade.color;
gradeText.textContent = `${overallGrade.grade} - ${overallGrade.description}`;
// Technical Debt
document.getElementById('technicalDebt').textContent = `${systemTrends.technicalDebt}%`;
document.getElementById('totalIssues').textContent = `${summary.totalIssues} total issues`;
// Flows
document.getElementById('totalFlows').textContent = summary.totalFlows;
document.getElementById('totalNodes').textContent = `${summary.totalNodes} function nodes`;
// Complexity
document.getElementById('avgComplexity').textContent = systemTrends.complexity;
}
updateQualityTrendChart(data) {
const ctx = document.getElementById('qualityTrendChart').getContext('2d');
if (this.charts.qualityTrend) {
this.charts.qualityTrend.destroy();
}
const chartData = data.trends.slice(-50).reverse(); // Last 50 points, chronological order
this.charts.qualityTrend = new Chart(ctx, {
type: 'line',
data: {
labels: chartData.map(point => new Date(point.created_at).toLocaleTimeString()),
datasets: [{
label: 'Quality Score',
data: chartData.map(point => point.quality_score),
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderWidth: 2,
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
grid: {
color: '#f3f4f6'
}
},
x: {
grid: {
color: '#f3f4f6'
}
}
},
elements: {
point: {
radius: 3,
hoverRadius: 6
}
}
}
});
}
async updatePerformanceChart(metricType) {
try {
const data = await this.fetchAPI(`/code-analyzer/api/dashboard/performance-metrics?type=${metricType}&count=50`);
const ctx = document.getElementById('performanceChart').getContext('2d');
if (this.charts.performance) {
this.charts.performance.destroy();
}
const chartData = data.metrics;
const label = metricType === 'cpu' ? 'CPU Usage (%)' : 'Memory Usage (%)';
const color = metricType === 'cpu' ? '#10b981' : '#f59e0b';
this.charts.performance = new Chart(ctx, {
type: 'line',
data: {
labels: chartData.map(point => new Date(point.created_at).toLocaleTimeString()),
datasets: [{
label: label,
data: chartData.map(point => point.value),
borderColor: color,
backgroundColor: color + '20',
borderWidth: 2,
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
max: metricType === 'cpu' ? 100 : 100,
grid: {
color: '#f3f4f6'
}
},
x: {
grid: {
color: '#f3f4f6'
}
}
},
elements: {
point: {
radius: 2,
hoverRadius: 5
}
}
}
});
} catch (error) {
console.error('Failed to update performance chart:', error);
}
}
updateFlowsList(flows) {
const container = document.getElementById('flowsList');
if (!flows || flows.length === 0) {
container.innerHTML = '<p class="text-gray-500 text-center py-4">No flows analyzed yet</p>';
return;
}
container.innerHTML = flows.slice(0, 10).map((flow) => {
const qualityColor = this.getQualityColor(flow.quality_score);
const progressBarColor = this.getProgressBarColor(flow.quality_score);
const healthPercentage = Math.round((flow.total_function_nodes - flow.nodes_with_issues) / Math.max(1, flow.total_function_nodes) * 100);
return `
<div class="bg-gray-50 rounded-lg overflow-hidden">
<div class="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-100 transition-colors flow-toggle"
data-flow-id="${flow.flow_id}">
<div class="flex-1">
<div class="flex items-center">
<h4 class="font-medium text-gray-900">${flow.flow_name || `Flow ${flow.flow_id.substring(0, 8)}`}</h4>
<span class="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
style="background-color: ${qualityColor}20; color: ${qualityColor}">
${flow.quality_score}%
</span>
<i class="fas fa-chevron-down ml-2 text-gray-400 transition-transform flow-chevron"></i>
</div>
<div class="mt-1 text-sm text-gray-600">
${flow.total_issues} issues in ${flow.total_function_nodes} nodes (${healthPercentage}% healthy)
</div>
<div class="mt-2 w-full bg-gray-200 rounded-full h-2">
<div class="h-2 rounded-full transition-all duration-500"
style="width: ${flow.quality_score}%; background-color: ${progressBarColor}"></div>
</div>
</div>
<div class="ml-4 text-right">
<div class="text-sm font-medium" style="color: ${qualityColor}">
${this.getQualityGrade(flow.quality_score).grade}
</div>
<div class="text-xs text-gray-500">
Complexity: ${flow.complexity_score}
</div>
</div>
</div>
<div class="flow-details hidden" data-flow-id="${flow.flow_id}">
<div class="px-4 pb-4">
<div class="text-center text-gray-500">
<div class="loading-spinner mx-auto mb-2" style="width: 20px; height: 20px;"></div>
Loading flow details...
</div>
</div>
</div>
</div>
`;
}).join('');
// Add event listeners after DOM is updated
container.querySelectorAll('.flow-toggle').forEach(element => {
element.addEventListener('click', () => {
const flowId = element.dataset.flowId;
this.toggleFlowDetails(flowId, element);
});
});
}
async toggleFlowDetails(flowId, element) {
const detailsContainer = element.parentElement.querySelector('.flow-details');
const chevron = element.querySelector('.flow-chevron');
if (detailsContainer.classList.contains('hidden')) {
// Expand details
detailsContainer.classList.remove('hidden');
chevron.style.transform = 'rotate(180deg)';
// Load flow details if not already loaded
if (!detailsContainer.dataset.loaded) {
await this.loadFlowDetails(flowId, detailsContainer);
detailsContainer.dataset.loaded = 'true';
}
} else {
// Collapse details
detailsContainer.classList.add('hidden');
chevron.style.transform = 'rotate(0deg)';
}
}
async loadFlowDetails(flowId, container) {
try {
const flowDetails = await this.fetchAPI(`/code-analyzer/api/dashboard/flows/${flowId}/details`);
container.innerHTML = `
<div class="px-4 pb-4 border-t border-gray-200 bg-white">
<div class="py-3">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div class="text-center">
<div class="text-2xl font-bold text-red-600">${flowDetails.criticalIssues}</div>
<div class="text-xs text-gray-500">Critical</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-orange-600">${flowDetails.warningIssues}</div>
<div class="text-xs text-gray-500">Warnings</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-blue-600">${flowDetails.infoIssues}</div>
<div class="text-xs text-gray-500">Info</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-gray-600">${flowDetails.healthPercentage}%</div>
<div class="text-xs text-gray-500">Healthy</div>
</div>
</div>
${flowDetails.recommendations.length > 0 ? `
<div class="mb-4 p-3 bg-blue-50 rounded-lg">
<h5 class="text-sm font-medium text-blue-900 mb-2">Recommendations:</h5>
<div class="space-y-1">
${flowDetails.recommendations.map(rec => `
<div class="flex items-start">
<i class="fas ${rec.type === 'critical' ? 'fa-exclamation-circle text-red-600' :
rec.type === 'warning' ? 'fa-exclamation-triangle text-orange-600' :
'fa-info-circle text-blue-600'} text-sm mt-0.5 mr-2"></i>
<div class="text-sm text-gray-700">
<span class="font-medium">${rec.message}</span> - ${rec.action}
</div>
</div>
`).join('')}
</div>
</div>
` : ''}
<div class="space-y-3">
<h5 class="text-sm font-semibold text-gray-900">Function Nodes (${flowDetails.nodes.length}):</h5>
${this.renderFlowNodes(flowDetails.nodes)}
</div>
</div>
</div>
`;
// Add event listeners to error items after DOM is updated
setTimeout(() => {
const errorItems = container.querySelectorAll('.error-item');
errorItems.forEach((errorElement) => {
errorElement.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const nodeId = errorElement.dataset.nodeId;
const flowId = errorElement.dataset.flowId;
const line = parseInt(errorElement.dataset.line);
const column = parseInt(errorElement.dataset.column);
const nodeName = errorElement.dataset.nodeName;
if (window.openNodeEditor) {
window.openNodeEditor(nodeId, flowId, line, column, nodeName);
} else {
console.error('openNodeEditor function not found on window object');
}
});
// Update title with helpful information
errorElement.title = `Click to open ${errorElement.dataset.nodeName} at line ${errorElement.dataset.line} in Node-RED editor`;
});
}, 100);
} catch (error) {
container.innerHTML = `
<div class="px-4 pb-4 border-t border-gray-200 bg-red-50">
<div class="text-red-600 text-sm py-2">
<i class="fas fa-exclamation-circle mr-2"></i>
Failed to load flow details: ${error.message}
</div>
</div>
`;
}
}
renderFlowNodes(nodes) {
if (!nodes || nodes.length === 0) {
return '<div class="text-gray-500 text-sm">No function nodes found</div>';
}
return nodes.map(node => {
const qualityColor = this.getQualityColor(node.qualityScore);
const hasIssues = node.issuesCount > 0;
return `
<div class="border border-gray-200 rounded-lg ${hasIssues ? 'bg-red-50' : 'bg-green-50'}">
<div class="p-3">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="flex items-center">
<h6 class="font-medium text-gray-900">${node.nodeName}</h6>
<span class="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
style="background-color: ${qualityColor}20; color: ${qualityColor}">
${node.qualityGrade.grade}
</span>
${node.criticalIssues > 0 ?
`<i class="fas fa-exclamation-circle text-red-600 ml-2" title="${node.criticalIssues} critical issues"></i>` :
node.warningIssues > 0 ?
`<i class="fas fa-exclamation-triangle text-orange-600 ml-2" title="${node.warningIssues} warnings"></i>` :
'<i class="fas fa-check-circle text-green-600 ml-2" title="No issues"></i>'
}
</div>
<div class="text-xs text-gray-600 mt-1">
${node.linesOfCode} lines • Complexity: ${node.complexityScore} • Quality: ${node.qualityScore}%
</div>
</div>
</div>
${hasIssues ? `
<div class="mt-3 space-y-2">
<h6 class="text-xs font-semibold text-gray-700 uppercase tracking-wide">
Issues (${node.issuesCount}):
</h6>
${node.issues.map(issue => `
<div class="flex items-start p-2 bg-white rounded border-l-4 ${
issue.severity === 'critical' ? 'border-red-500' :
issue.severity === 'warning' ? 'border-orange-500' :
'border-blue-500'
} hover:bg-gray-50 cursor-pointer transition-colors group error-item"
data-node-id="${node.nodeId}"
data-flow-id="${node.navigation.flowId}"
data-line="${issue.line}"
data-column="${issue.column || 1}"
data-node-name="${node.nodeName.replace(/"/g, '"')}"
title="Click to open in Node-RED editor (Line ${issue.line})">
<div class="flex-shrink-0 mt-0.5">
<span class="inline-flex items-center justify-center w-5 h-5 rounded-full text-xs font-bold text-white"
style="background-color: ${issue.color}">
${issue.priority}
</span>
</div>
<div class="ml-2 flex-1">
<div class="flex items-center">
<span class="text-xs font-semibold uppercase tracking-wide"
style="color: ${issue.color}">
${issue.severity}
</span>
<span class="ml-2 text-xs text-gray-500 group-hover:text-blue-600">
<i class="fas fa-external-link-alt mr-1 opacity-0 group-hover:opacity-100 transition-opacity"></i>
Line ${issue.line}${issue.column ? `, Col ${issue.column}` : ''}
</span>
<span class="ml-2 text-xs text-gray-400">
-${issue.weight} pts
</span>
</div>
<div class="text-sm text-gray-800 mt-1 group-hover:text-gray-900">
${issue.message}
</div>
<div class="text-xs text-gray-500 mt-1 group-hover:text-gray-600">
Type: ${issue.type.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
<span class="ml-2 text-blue-600 opacity-0 group-hover:opacity-100 transition-opacity">
→ Click to open in editor
</span>
</div>
</div>
</div>
`).join('')}
</div>
` : `
<div class="mt-3 p-2 bg-green-100 rounded-lg text-center">
<i class="fas fa-check-circle text-green-600 mr-2"></i>
<span class="text-sm text-green-800 font-medium">No issues found - excellent code quality!</span>
</div>
`}
</div>
</div>
`;
}).join('');
}
updateProblematicNodesList(nodes) {
const container = document.getElementById('problematicNodesList');
if (!nodes || nodes.length === 0) {
container.innerHTML = '<p class="text-gray-500 text-center py-4">No problematic nodes found</p>';
return;
}
container.innerHTML = nodes.map(node => {
const qualityColor = this.getQualityColor(node.quality_score);
const severityIcon = node.issues_count >= 5 ? 'fa-exclamation-circle' :
node.issues_count >= 3 ? 'fa-exclamation-triangle' : 'fa-info-circle';
const severityColor = node.issues_count >= 5 ? '#ef4444' :
node.issues_count >= 3 ? '#f59e0b' : '#3b82f6';
return `
<div class="flex items-center p-4 bg-gray-50 rounded-lg">
<div class="flex-shrink-0">
<i class="fas ${severityIcon} text-lg" style="color: ${severityColor}"></i>
</div>
<div class="ml-4 flex-1">
<div class="flex items-center">
<h4 class="font-medium text-gray-900">${node.node_name}</h4>
<span class="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
style="background-color: ${qualityColor}20; color: ${qualityColor}">
${node.quality_score}%
</span>
</div>
<div class="mt-1 text-sm text-gray-600">
${node.issues_count} issues • ${node.lines_of_code} lines • Complexity: ${node.complexity_score}
</div>
</div>
<div class="ml-4 text-right">
<div class="text-sm font-medium" style="color: ${qualityColor}">
${this.getQualityGrade(node.quality_score).grade}
</div>
</div>
</div>
`;
}).join('');
}
updateAlertsList(alerts) {
const container = document.getElementById('alertsList');
if (!alerts || alerts.length === 0) {
container.innerHTML = '<p class="text-gray-500 text-center py-4">No recent alerts</p>';
return;
}
container.innerHTML = alerts.slice(0, 10).map(alert => {
const alertTime = new Date(alert.created_at).toLocaleString();
const alertIcon = this.getAlertIcon(alert.metric_type);
const alertColor = this.getAlertColor(alert.current_value, alert.threshold_value);
return `
<div class="flex items-center p-3 border border-gray-200 rounded-lg">
<div class="flex-shrink-0">
<i class="fas ${alertIcon} text-lg" style="color: ${alertColor}"></i>
</div>
<div class="ml-3 flex-1">
<div class="text-sm font-medium text-gray-900">
${alert.metric_type.toUpperCase()} Alert
</div>
<div class="text-sm text-gray-600">
${alert.current_value}% (threshold: ${alert.threshold_value}%) for ${alert.duration_minutes.toFixed(1)} minutes
</div>
</div>
<div class="ml-3 text-right">
<div class="text-xs text-gray-500">${alertTime}</div>
</div>
</div>
`;
}).join('');
}
updateTimeframe(hours) {
this.currentTimeframe = hours;
// Update button states
document.querySelectorAll('.trend-btn').forEach(btn => {
btn.classList.remove('bg-blue-100', 'text-blue-800');
btn.classList.add('bg-gray-100', 'text-gray-600');
});
document.querySelector(`[data-hours="${hours}"]`).classList.remove('bg-gray-100', 'text-gray-600');
document.querySelector(`[data-hours="${hours}"]`).classList.add('bg-blue-100', 'text-blue-800');
// Reload quality trends
this.loadQualityTrends();
}
async loadQualityTrends() {
try {
const qualityTrends = await this.fetchAPI(`/code-analyzer/api/dashboard/quality-trends?hours=${this.currentTimeframe}`);
this.updateQualityTrendChart(qualityTrends);
} catch (error) {
console.error('Failed to load quality trends:', error);
}
}
updateLastUpdated(timestamp) {
document.getElementById('lastUpdated').textContent = new Date(timestamp).toLocaleTimeString();
}
showLoading(show) {
document.getElementById('loadingState').classList.toggle('hidden', !show);
document.getElementById('dashboardContent').classList.toggle('hidden', show);
}
showError(message) {
// You could implement a toast notification here
console.error(message);
}
startAutoRefresh() {
// Refresh every 5 minutes
this.refreshInterval = setInterval(() => {
this.loadDashboardData();
}, 5 * 60 * 1000);
}
stopAutoRefresh() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
}
// Navigate to Node-RED editor for specific node and line
async openNodeEditor(nodeId, flowId, lineNumber = 1, columnNumber = 1, nodeName = 'Function Node') {
console.log('openNodeEditor called with:', { nodeId, flowId, lineNumber, columnNumber, nodeName }); // Debug log
try {
// Construct the Node-RED editor URL (try without /red/ prefix)
const editorUrl = `/#flow/${flowId}`;
// First, navigate to the flow
const newWindow = window.open(editorUrl, '_blank');
if (!newWindow) {
// Popup blocked, try alternative approach
this.showNavigationModal(flowId, lineNumber, columnNumber, nodeName);
return;
}
// Try different approaches to open the node
this.attemptNodeNavigation(newWindow, nodeId, flowId, lineNumber, columnNumber);
// Always show a helpful toast for user guidance
setTimeout(() => {
this.showNavigationToast(nodeName, lineNumber, nodeId);
}, 1000);
} catch (error) {
console.error('Failed to open Node-RED editor:', error);
this.showError('Failed to open Node-RED editor: ' + error.message);
}
}
// Smart single-attempt navigation to prevent multiple editor instances
attemptNodeNavigation(nodeRedWindow, nodeId, flowId, lineNumber, columnNumber) {
let navigationCompleted = false;
// Single optimized attempt with smart Monaco editor handling
setTimeout(() => {
try {
if (nodeRedWindow.closed || navigationCompleted) {
return;
}
const script = `
(function() {
if (typeof RED !== 'undefined' && RED.nodes && RED.editor) {
const targetNode = RED.nodes.node('${nodeId}');
if (targetNode) {
RED.editor.edit(targetNode);
const waitForActiveEditor = (attempts = 0) => {
if (attempts > 15) return;
if (typeof monaco !== 'undefined' && monaco.editor) {
const editors = monaco.editor.getEditors();
if (editors.length > 0) {
let activeEditor = editors.find(editor => {
return editor.hasWidgetFocus() || editor.hasTextFocus();
});
if (!activeEditor) {
activeEditor = editors[editors.length - 1];
}
if (activeEditor && activeEditor.getModel()) {
activeEditor.focus();
activeEditor.revealLineInCenter(${lineNumber});
activeEditor.setPosition({
lineNumber: ${lineNumber},
column: ${columnNumber}
});
const decoration = activeEditor.deltaDecorations([], [{
range: new monaco.Range(${lineNumber}, 1, ${lineNumber}, 100),
options: {
className: 'highlighted-line-error',
isWholeLine: true,
glyphMarginClassName: 'error-glyph-margin'
}
}]);
setTimeout(() => {
if (activeEditor && !activeEditor.isDisposed()) {
activeEditor.deltaDecorations(decoration, []);
}
}, 8000);
return;
}
}
}
setTimeout(() => waitForActiveEditor(attempts + 1), 500);
};
setTimeout(() => waitForActiveEditor(), 800);
}
}
})();
`;
nodeRedWindow.eval(script);
navigationCompleted = true;
} catch (error) {
// Navigation attempt failed silently
}
}, 1500); // Single optimized delay
// Backup URL navigation
setTimeout(() => {
try {
const enhancedUrl = `/#flow/${flowId}?node=${nodeId}&line=${lineNumber}`;
nodeRedWindow.location.href = enhancedUrl;
} catch (error) {
// URL navigation failed silently
}
}, 2000);
}
showNavigationModal(flowId, lineNumber, columnNumber, nodeName) {
// Create a modal with instructions if automatic navigation fails
const modalHTML = `
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
<div class="bg-white rounded-lg shadow-xl p-6 max-w-md mx-4">
<div class="flex items-center mb-4">
<i class="fas fa-external-link-alt text-blue-600 text-xl mr-3"></i>
<h3 class="text-lg font-semibold text-gray-900">Navigate to Error</h3>
</div>
<div class="mb-4 text-sm text-gray-600">
<p class="mb-2">To view this error in the Node-RED editor:</p>
<ol class="list-decimal list-inside space-y-1 text-xs bg-gray-50 p-3 rounded">
<li>Open Node-RED: <a href="/#flow/${flowId}" target="_blank" class="text-blue-600 hover:underline">/#flow/${flowId}</a></li>
<li>Find and double-click: <strong>${nodeName}</strong></li>
<li>Navigate to: <strong>Line ${lineNumber}</strong>${columnNumber > 1 ? `, Column ${columnNumber}` : ''}</li>
</ol>
</div>
<div class="flex justify-between">
<button onclick="window.open('/#flow/${flowId}', '_blank')"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm">
<i class="fas fa-external-link-alt mr-2"></i>Open Flow
</button>
<button onclick="this.parentElement.parentElement.parentElement.remove()"
class="px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400 text-sm">
Close
</button>
</div>
</div>
</div>
`;
// Add modal to page
document.body.insertAdjacentHTML('beforeend', modalHTML);
}
showNavigationToast(nodeName, lineNumber, nodeId) {
// Show a toast notification with navigation info
const toastHTML = `
<div class="fixed top-4 right-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white px-6 py-4 rounded-lg shadow-lg z-50 max-w-md navigation-toast">
<div class="flex items-start">
<i class="fas fa-external-link-alt text-blue-200 text-lg mt-0.5 mr-3"></i>
<div class="flex-1">
<div class="text-sm font-semibold mb-2">Node-RED Editor Opened</div>
<div class="text-xs text-blue-100 space-y-1">
<div>• <strong>Node:</strong> ${nodeName}</div>
<div>• <strong>Line:</strong> ${lineNumber}</div>
<div>• <strong>Node ID:</strong> ${nodeId.substring(0, 8)}...</div>
</div>
<div class="text-xs text-blue-200 mt-2 pt-2 border-t border-blue-500">
<strong>Next steps:</strong> Double-click the function node to open its editor
</div>
</div>
<button onclick="this.parentElement.parentElement.remove()"
class="ml-2 text-blue-200 hover:text-white transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
</div>
`;
// Add toast to page
document.body.insertAdjacentHTML('beforeend', toastHTML);
// Remove toast after 10 seconds
setTimeout(() => {
const toast = document.querySelector('.navigation-toast');
if (toast) {
toast.classList.add('opacity-0');
setTimeout(() => toast.remove(), 300);
}
}, 10000);
}
// Utility functions
getQualityColor(score) {
if (score >= 90) return '#10b981';
if (score >= 75) return '#84cc16';
if (score >= 60) return '#f59e0b';
return '#ef4444';
}
// Progress bar colors based on quality score (more granular than getQualityColor)
getProgressBarColor(score) {
// A+ (98-100): Pure green
if (score >= 98) return '#22c55e';
// A (95-97): Green
if (score >= 95) return '#16a34a';
// A- (90-94): Green with slight yellow tint
if (score >= 90) return '#65a30d';
// B+ (85-89): Yellow-green (similar to A-)
if (score >= 85) return '#84cc16';
// B (80-84): Yellow
if (score >= 80) return '#eab308';
// B- (70-79): Yellow-orange
if (score >= 70) return '#f59e0b';
// C+ (60-69): Orange
if (score >= 60) return '#f97316';
// C (50-59): Orange-red
if (score >= 50) return '#ea580c';
// D (35-49): Red-orange
if (score >= 35) return '#dc2626';
// D- (20-34): Red
if (score >= 20) return '#b91c1c';
// F (0-19): Dark red
return '#991b1b';
}
getQualityGrade(score) {
if (score >= 95) return { grade: 'A+', color: '#22c55e' };
if (score >= 90) return { grade: 'A', color: '#16a34a' };
if (score >= 85) return { grade: 'A-', color: '#65a30d' };
if (score >= 80) return { grade: 'B+', color: '#84cc16' };
if (score >= 75) return { grade: 'B', color: '#eab308' };
if (score >= 70) return { grade: 'B-', color: '#f59e0b' };
if (score >= 65) return { grade: 'C+', color: '#f97316' };
if (score >= 60) return { grade: 'C', color: '#ea580c' };
if (score >= 50) return { grade: 'C-', color: '#dc2626' };
return { grade: 'F', color: '#991b1b' };
}
getAlertIcon(metricType) {
switch (metricType) {
case 'cpu': return 'fa-microchip';
case 'memory': return 'fa-memory';
case 'eventLoop': return 'fa-clock';
default: return 'fa-exclamation-triangle';
}
}
getAlertColor(currentValue, thresholdValue) {
const ratio = currentValue / thresholdValue;
if (ratio >= 1.5) return '#dc2626';
if (ratio >= 1.2) return '#f59e0b';
return '#3b82f6';
}
}
// Initialize dashboard when page loads
document.addEventListener('DOMContentLoaded', () => {
const dashboard = new QualityDashboard();
// Make navigation function available globally for onclick handlers
window.openNodeEditor = (nodeId, flowId, lineNumber, columnNumber, nodeName) => {
dashboard.openNodeEditor(nodeId, flowId, parseInt(lineNumber), parseInt(columnNumber), nodeName);
};
});