Created
April 21, 2025 08:16
-
-
Save kchaitanya863/ba7292d56a089fc47e6f906b4e58c4b5 to your computer and use it in GitHub Desktop.
Standalone Azure Application Insights Query & Visualization Tool
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>App Insights Query & Visualize Tool</title> | |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"/> | |
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3/dist/chartjs-adapter-date-fns.bundle.min.js"></script> <style> | |
body { padding: 1em; } | |
.container { max-width: 1200px; margin: auto; } /* Wider container */ | |
hgroup { margin-bottom: 1.5em; } | |
section { margin-bottom: 2em; padding-bottom: 1em; border-bottom: 1px solid var(--pico-border); } | |
section:last-of-type { border-bottom: none; } | |
#results-output article { padding: 1em; } | |
table { width: 100%; font-size: 0.9em; } | |
th, td { padding: 0.5em 0.75em; white-space: nowrap; } /* Prevent wrapping */ | |
.error-message, .success-message { padding: 0.5em; border: 1px solid; border-radius: var(--pico-border-radius); margin-top: 0.5em; } | |
.error-message { color: var(--pico-color-red-600); background-color: var(--pico-color-red-100); border-color: var(--pico-color-red-300); } | |
.success-message { color: var(--pico-color-green-700); background-color: var(--pico-color-green-100); border-color: var(--pico-color-green-300); } | |
.warning-message { color: var(--pico-color-amber-700); background-color: var(--pico-color-amber-100); padding: 1em; border: 1px solid var(--pico-color-amber-300); border-radius: var(--pico-border-radius); margin-bottom: 1em; font-weight: bold; } | |
textarea { min-height: 150px; font-family: monospace; } | |
#chart-container { position: relative; min-height: 300px; max-height: 500px; width: 100%; } /* Relative for Chart.js */ | |
#visualization-options { display: none; margin-top: 1em; padding: 1em; border: 1px dashed var(--pico-secondary-border); border-radius: var(--pico-border-radius); } | |
#visualization-options .grid label { margin-bottom: 0; } /* Tighter grid */ | |
#stored-results-list ul { list-style: none; padding-left: 0; } | |
#stored-results-list li { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5em; padding: 0.5em; background-color: var(--pico-card-background-color); border: 1px solid var(--pico-card-border-color); border-radius: var(--pico-border-radius); } | |
#stored-results-list li span { flex-grow: 1; margin-right: 1em; font-size: 0.9em; } | |
#stored-results-list li button { font-size: 0.8em; padding: 0.2em 0.5em; margin-left: 0.5em; background-color: var(--pico-color-red-500); border-color: var(--pico-color-red-500); color: white; } | |
.loading-indicator { display: inline-block; margin-left: 10px; } /* Simple text loading */ | |
small { color: var(--pico-secondary); } | |
</style> | |
</head> | |
<body> | |
<main class="container"> | |
<hgroup> | |
<h1>App Insights Query & Visualize Tool</h1> | |
<p>Run KQL queries, store results locally, and create visualizations.</p> | |
</hgroup> | |
<article id="security-warning" class="warning-message"> | |
<strong>Security Warning:</strong> Your Application Insights API Key will be stored in your browser's Local Storage and sent directly from the browser. This is insecure and exposes the key. Use only for non-sensitive data or personal testing where you accept the risk. Do NOT use this in shared environments. | |
</article> | |
<section id="connections-section"> | |
<details> <summary><h2>Manage Connections</h2></summary> | |
<form id="connection-form"> | |
<div class="grid"> | |
<label for="conn-name"> Connection Name <input type="text" id="conn-name" name="conn-name" placeholder="My App Prod" required> </label> | |
<label for="conn-appid"> App ID (GUID) <input type="text" id="conn-appid" name="conn-appid" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required> </label> | |
</div> | |
<label for="conn-apikey"> API Key <input type="password" id="conn-apikey" name="conn-apikey" placeholder="Your App Insights API Key" required> <small>Warning: Stored locally in browser!</small> </label> | |
<button type="submit">Save Connection</button> | |
</form> | |
<div id="connections-list" style="margin-top: 1em;"> | |
<h3>Saved Connections:</h3> | |
<ul id="connections-ul"></ul> | |
<p id="no-connections-msg">No connections saved yet.</p> | |
</div> | |
</details> | |
</section> | |
<section id="query-section"> | |
<h2>Run Query & Store Result</h2> | |
<form id="query-form"> | |
<div class="grid"> | |
<label for="query-connection">Select Connection | |
<select id="query-connection" name="query-connection" required> | |
<option value="" disabled selected>-- Select --</option> | |
</select> | |
</label> | |
<label for="query-save-name">Save Result As (Optional) | |
<input type="text" id="query-save-name" name="query-save-name" placeholder="e.g., Daily Errors Last 7d"> | |
</label> | |
</div> | |
<label for="query-kql">KQL Query</label> | |
<textarea id="query-kql" name="query-kql" placeholder="requests | take 10" required></textarea> | |
<button type="submit" id="run-query-btn" aria-busy="false">Run Query</button> | |
<span id="query-status"></span> | |
</form> | |
</section> | |
<section id="stored-results-section"> | |
<h2>Stored Query Results</h2> | |
<div id="stored-results-list"> | |
<ul id="results-ul"></ul> | |
<p id="no-results-msg">No results saved yet.</p> | |
</div> | |
<button id="clear-results-btn" class="secondary outline" style="margin-top: 1em;">Clear All Stored Results</button> | |
</section> | |
<section id="visualization-section"> | |
<h2>Visualize Stored Data</h2> | |
<form id="visualization-form"> | |
<label for="viz-result-select">Select Stored Result</label> | |
<select id="viz-result-select" name="viz-result-select" required> | |
<option value="" disabled selected>-- Select Result --</option> | |
</select> | |
<div id="visualization-options"> <label for="viz-type">Visualization Type</label> | |
<select id="viz-type" name="viz-type"> | |
<option value="table" selected>Table</option> | |
<option value="chart-line">Line Chart</option> | |
<option value="chart-bar">Bar Chart</option> | |
<option value="chart-pie">Pie Chart</option> | |
</select> | |
<div class="grid" id="chart-axis-selectors" style="display: none; margin-top: 1em;"> <label for="viz-xaxis-col">X-Axis Column | |
<select id="viz-xaxis-col" name="viz-xaxis-col"></select> | |
</label> | |
<label for="viz-yaxis-col">Y-Axis Column (Value) | |
<select id="viz-yaxis-col" name="viz-yaxis-col"></select> | |
</label> | |
</div> | |
<button type="submit" id="generate-viz-btn" style="margin-top: 1em;">Generate Visualization</button> | |
</div> | |
</form> | |
</section> | |
<section id="results-output-section"> | |
<h2>Visualization Output</h2> | |
<article id="results-output"> | |
<p>Select a stored result and configure visualization options above.</p> | |
<div id="table-container" style="overflow-x: auto;"></div> | |
<div id="chart-container" style="display: none;"> | |
<canvas id="results-chart"></canvas> | |
</div> | |
<div id="error-output"></div> | |
</article> | |
</section> | |
</main> | |
<script> | |
// --- Constants --- | |
const CONNECTIONS_STORAGE_KEY = 'appInsightsConnections'; | |
const RESULTS_STORAGE_KEY = 'appInsightsQueryResults'; | |
// --- DOM Elements --- | |
// Connections | |
const connectionForm = document.getElementById('connection-form'); | |
const connNameInput = document.getElementById('conn-name'); | |
const connAppIdInput = document.getElementById('conn-appid'); | |
const connApiKeyInput = document.getElementById('conn-apikey'); | |
const connectionsListUl = document.getElementById('connections-ul'); | |
const noConnectionsMsg = document.getElementById('no-connections-msg'); | |
const queryConnectionSelect = document.getElementById('query-connection'); | |
// Querying | |
const queryForm = document.getElementById('query-form'); | |
const queryKqlTextarea = document.getElementById('query-kql'); | |
const querySaveNameInput = document.getElementById('query-save-name'); | |
const runQueryBtn = document.getElementById('run-query-btn'); | |
const queryStatus = document.getElementById('query-status'); | |
// Stored Results | |
const storedResultsUl = document.getElementById('results-ul'); | |
const noResultsMsg = document.getElementById('no-results-msg'); | |
const clearResultsBtn = document.getElementById('clear-results-btn'); | |
// Visualization | |
const visualizationForm = document.getElementById('visualization-form'); | |
const vizResultSelect = document.getElementById('viz-result-select'); | |
const vizOptionsDiv = document.getElementById('visualization-options'); | |
const vizTypeSelect = document.getElementById('viz-type'); | |
const chartAxisSelectors = document.getElementById('chart-axis-selectors'); | |
const vizXAxisSelect = document.getElementById('viz-xaxis-col'); | |
const vizYAxisSelect = document.getElementById('viz-yaxis-col'); | |
const generateVizBtn = document.getElementById('generate-viz-btn'); | |
// Output | |
const resultsOutput = document.getElementById('results-output'); | |
const tableContainer = document.getElementById('table-container'); | |
const chartContainer = document.getElementById('chart-container'); | |
const chartCanvas = document.getElementById('results-chart'); | |
const errorOutput = document.getElementById('error-output'); | |
let chartInstance = null; // To hold the Chart.js instance | |
// --- State Management (localStorage) --- | |
// Connections | |
function getConnections() { | |
const connectionsJson = localStorage.getItem(CONNECTIONS_STORAGE_KEY); | |
return connectionsJson ? JSON.parse(connectionsJson) : []; | |
} | |
function saveConnections(connections) { | |
localStorage.setItem(CONNECTIONS_STORAGE_KEY, JSON.stringify(connections)); | |
renderConnections(); | |
} | |
function addConnection(name, appId, apiKey) { | |
const connections = getConnections(); | |
const id = `conn-${Date.now()}`; | |
if (connections.some(c => c.name === name)) { | |
alert(`Connection name "${name}" already exists.`); return; | |
} | |
connections.push({ id, name, appId, apiKey }); | |
saveConnections(connections); | |
} | |
function deleteConnection(id) { | |
let connections = getConnections(); | |
connections = connections.filter(c => c.id !== id); | |
saveConnections(connections); | |
} | |
// Stored Results | |
function getStoredResults() { | |
const resultsJson = localStorage.getItem(RESULTS_STORAGE_KEY); | |
return resultsJson ? JSON.parse(resultsJson) : []; | |
} | |
function saveStoredResults(results) { | |
try { | |
localStorage.setItem(RESULTS_STORAGE_KEY, JSON.stringify(results)); | |
} catch (e) { | |
// Handle potential storage quota exceeded error | |
console.error("Error saving results to localStorage:", e); | |
alert("Error saving results: LocalStorage quota might be exceeded. Clear some stored results."); | |
// Optionally, try to remove the oldest result? | |
if (e.name === 'QuotaExceededError' && results.length > 0) { | |
console.warn("Attempting to remove oldest result to free space..."); | |
saveStoredResults(results.slice(1)); // Remove the first (oldest) item | |
} | |
} | |
renderStoredResults(); | |
} | |
function addStoredResult(result) { | |
const results = getStoredResults(); | |
// Add metadata if not present | |
if (!result.id) result.id = `res-${Date.now()}`; | |
if (!result.timestamp) result.timestamp = new Date().toISOString(); | |
results.push(result); | |
saveStoredResults(results); | |
} | |
function deleteStoredResult(id) { | |
let results = getStoredResults(); | |
results = results.filter(r => r.id !== id); | |
saveStoredResults(results); | |
// If the deleted result was selected, reset visualization form | |
if (vizResultSelect.value === id) { | |
vizResultSelect.value = ""; | |
vizOptionsDiv.style.display = 'none'; | |
clearVisualizationOutput(); | |
} | |
} | |
function clearAllStoredResults() { | |
if (confirm("Are you sure you want to delete ALL stored query results? This cannot be undone.")) { | |
saveStoredResults([]); | |
vizResultSelect.value = ""; | |
vizOptionsDiv.style.display = 'none'; | |
clearVisualizationOutput(); | |
} | |
} | |
// --- Rendering --- | |
function renderConnections() { | |
const connections = getConnections(); | |
connectionsListUl.innerHTML = ''; | |
queryConnectionSelect.innerHTML = '<option value="" disabled selected>-- Select --</option>'; | |
noConnectionsMsg.style.display = connections.length === 0 ? 'block' : 'none'; | |
connections.forEach(conn => { | |
const li = document.createElement('li'); | |
li.innerHTML = `<span>${conn.name} <small>(App ID: ${conn.appId.substring(0, 8)}...)</small></span>`; | |
const deleteBtn = document.createElement('button'); | |
deleteBtn.textContent = 'Delete'; | |
deleteBtn.onclick = () => { if (confirm(`Delete connection "${conn.name}"?`)) deleteConnection(conn.id); }; | |
li.appendChild(deleteBtn); | |
connectionsListUl.appendChild(li); | |
const option = document.createElement('option'); | |
option.value = conn.id; | |
option.textContent = conn.name; | |
queryConnectionSelect.appendChild(option); | |
}); | |
} | |
function renderStoredResults() { | |
const results = getStoredResults(); | |
storedResultsUl.innerHTML = ''; | |
vizResultSelect.innerHTML = '<option value="" disabled selected>-- Select Result --</option>'; // Reset viz dropdown | |
noResultsMsg.style.display = results.length === 0 ? 'block' : 'none'; | |
results.forEach(res => { | |
// List item | |
const li = document.createElement('li'); | |
const nameText = res.name || `Result from ${new Date(res.timestamp).toLocaleString()}`; | |
const rowCount = res.data?.tables?.[0]?.rows?.length ?? 0; | |
li.innerHTML = `<span>${nameText} <small>(${rowCount} rows, Query: ${res.query.substring(0, 30)}...)</small></span>`; | |
const deleteBtn = document.createElement('button'); | |
deleteBtn.textContent = 'Delete'; | |
deleteBtn.onclick = () => deleteStoredResult(res.id); | |
li.appendChild(deleteBtn); | |
storedResultsUl.appendChild(li); | |
// Viz dropdown option | |
const option = document.createElement('option'); | |
option.value = res.id; | |
option.textContent = nameText; | |
vizResultSelect.appendChild(option); | |
}); | |
} | |
function displayQueryStatus(message, isError = false) { | |
queryStatus.textContent = message; | |
queryStatus.className = isError ? 'error-message' : 'success-message'; | |
// Clear message after a few seconds | |
setTimeout(() => { queryStatus.textContent = ''; queryStatus.className = ''; }, isError ? 5000 : 3000); | |
} | |
function clearVisualizationOutput() { | |
tableContainer.innerHTML = ''; | |
chartContainer.style.display = 'none'; | |
if (chartInstance) { | |
chartInstance.destroy(); | |
chartInstance = null; | |
} | |
errorOutput.innerHTML = ''; | |
const placeholder = resultsOutput.querySelector('p'); | |
if(!placeholder) { // Add placeholder back if it was removed | |
const p = document.createElement('p'); | |
p.textContent = 'Select a stored result and configure visualization options above.'; | |
resultsOutput.prepend(p); | |
} | |
} | |
function renderErrorInOutput(message) { | |
clearVisualizationOutput(); | |
errorOutput.innerHTML = `<div class="error-message">${message}</div>`; | |
console.error("Visualization Error:", message); | |
const placeholder = resultsOutput.querySelector('p'); | |
if(placeholder) placeholder.remove(); | |
} | |
function renderTable(resultData) { | |
clearVisualizationOutput(); | |
const placeholder = resultsOutput.querySelector('p'); | |
if(placeholder) placeholder.remove(); | |
if (!resultData || !resultData.tables || resultData.tables.length === 0) { | |
tableContainer.innerHTML = '<p>No data available in the selected result.</p>'; | |
return; | |
} | |
const tableData = resultData.tables[0]; // Assume first table | |
const columns = tableData.columns; | |
const rows = tableData.rows; | |
const table = document.createElement('table'); | |
const thead = document.createElement('thead'); | |
const tbody = document.createElement('tbody'); | |
table.className = "striped"; // Pico class | |
// Header row | |
const headerRow = document.createElement('tr'); | |
columns.forEach(col => { | |
const th = document.createElement('th'); | |
th.textContent = `${col.name}`; | |
th.title = `Type: ${col.type}` | |
th.scope = 'col'; | |
headerRow.appendChild(th); | |
}); | |
thead.appendChild(headerRow); | |
// Data rows | |
rows.forEach(row => { | |
const tr = document.createElement('tr'); | |
row.forEach(cell => { | |
const td = document.createElement('td'); | |
td.textContent = typeof cell === 'object' ? JSON.stringify(cell) : String(cell ?? ''); // Handle null/undefined | |
tr.appendChild(td); | |
}); | |
tbody.appendChild(tr); | |
}); | |
if (rows.length === 0) { | |
const tr = document.createElement('tr'); | |
const td = document.createElement('td'); | |
td.colSpan = columns.length; | |
td.textContent = 'Stored result contains no rows.'; | |
td.style.textAlign = 'center'; | |
tr.appendChild(td); | |
tbody.appendChild(tr); | |
} | |
table.appendChild(thead); | |
table.appendChild(tbody); | |
tableContainer.appendChild(table); | |
} | |
function renderChart(resultData, vizType, xAxisColName, yAxisColName) { | |
clearVisualizationOutput(); | |
chartContainer.style.display = 'block'; | |
const placeholder = resultsOutput.querySelector('p'); | |
if(placeholder) placeholder.remove(); | |
if (!resultData || !resultData.tables || resultData.tables.length === 0 || resultData.tables[0].rows.length === 0) { | |
renderErrorInOutput('No data available in the selected result to render chart.'); | |
chartContainer.style.display = 'none'; | |
return; | |
} | |
if (!xAxisColName || !yAxisColName) { | |
renderErrorInOutput('Please select columns for both X and Y axes for the chart.'); | |
chartContainer.style.display = 'none'; | |
return; | |
} | |
const tableData = resultData.tables[0]; | |
const columns = tableData.columns; | |
const rows = tableData.rows; | |
const xAxisIndex = columns.findIndex(c => c.name === xAxisColName); | |
const yAxisIndex = columns.findIndex(c => c.name === yAxisColName); | |
if (xAxisIndex === -1 || yAxisIndex === -1) { | |
renderErrorInOutput(`Selected column names ('${xAxisColName}', '${yAxisColName}') not found in data.`); | |
chartContainer.style.display = 'none'; | |
return; | |
} | |
const xAxisType = columns[xAxisIndex].type; | |
const yAxisType = columns[yAxisIndex].type; | |
// Basic check for Y-axis numeric type | |
if (!['real', 'long', 'int', 'decimal'].includes(yAxisType.toLowerCase())) { | |
console.warn(`Y-axis column '${yAxisColName}' has type '${yAxisType}', attempting to parse as number.`); | |
} | |
// --- Data Transformation for Chart.js --- | |
let chartLabels = []; | |
let chartDataValues = []; | |
let chartDataType = 'category'; // Default axis type | |
try { | |
// Sort data for line charts if X axis is time | |
if (vizType === 'line' && xAxisType.toLowerCase() === 'datetime') { | |
rows.sort((a, b) => new Date(a[xAxisIndex]) - new Date(b[xAxisIndex])); | |
} | |
rows.forEach(row => { | |
chartLabels.push(row[xAxisIndex]); | |
const yVal = parseFloat(row[yAxisIndex]); // Attempt to convert Y to number | |
chartDataValues.push(isNaN(yVal) ? null : yVal); // Push null if conversion fails | |
}); | |
// Check if X axis looks like time data | |
if (xAxisType.toLowerCase() === 'datetime') { | |
// Ensure labels are actual Date objects or compatible strings for time adapter | |
chartLabels = chartLabels.map(label => label ? new Date(label) : null); // Convert to Date objects | |
chartDataType = 'time'; | |
} | |
} catch (e) { | |
renderErrorInOutput(`Error processing data for chart: ${e.message}`); | |
chartContainer.style.display = 'none'; | |
return; | |
} | |
// --- End Data Transformation --- | |
const chartConfig = { | |
type: vizType, // 'bar', 'line', 'pie' | |
data: { | |
labels: chartLabels, | |
datasets: [{ | |
label: `${yAxisColName} by ${xAxisColName}`, | |
data: chartDataValues, | |
borderWidth: vizType === 'line' ? 2 : 1, | |
fill: vizType === 'line', // Fill area for line charts | |
// Add dynamic colors later? | |
backgroundColor: vizType === 'pie' ? generateColors(chartLabels.length) : undefined, // Colors for pie | |
}] | |
}, | |
options: { | |
responsive: true, | |
maintainAspectRatio: false, // Allow chart to fill container height | |
scales: (vizType !== 'pie') ? { | |
x: { | |
type: chartDataType, // 'category' or 'time' | |
title: { display: true, text: xAxisColName }, | |
time: (chartDataType === 'time') ? { | |
// tooltipFormat: 'PPpp', // Customize tooltip format if needed | |
unit: 'day' // Auto-detect or set based on data range | |
} : undefined, | |
}, | |
y: { | |
beginAtZero: true, | |
title: { display: true, text: yAxisColName } | |
} | |
} : {}, | |
plugins: { | |
title: { | |
display: true, | |
text: `Chart: ${yAxisColName} by ${xAxisColName}` | |
}, | |
legend: { | |
display: vizType === 'pie' || false, // Only show legend for Pie for now | |
position: 'top', | |
}, | |
tooltip: { | |
enabled: true, | |
} | |
} | |
} | |
}; | |
chartInstance = new Chart(chartCanvas, chartConfig); | |
} | |
// Helper function to generate distinct colors for Pie charts | |
function generateColors(count) { | |
const colors = []; | |
for (let i = 0; i < count; i++) { | |
// Simple HSL-based color generation | |
const hue = (i * (360 / (count * 1.1))) % 360; // Spread hues | |
colors.push(`hsl(${hue}, 70%, 60%)`); | |
} | |
return colors; | |
} | |
// --- API Call --- | |
async function executeAppInsightsQuery(appId, apiKey, kqlQuery) { | |
const endpoint = `https://api.applicationinsights.io/v1/apps/${appId}/query`; | |
console.log(`Querying endpoint: ${endpoint}`); | |
console.warn("Sending API Key directly from browser!"); // Security Reminder | |
try { | |
const response = await fetch(endpoint, { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey }, | |
body: JSON.stringify({ query: kqlQuery }), | |
}); | |
if (!response.ok) { | |
const errorBody = await response.text(); | |
throw new Error(`API Error (${response.status}): ${errorBody || response.statusText}`); | |
} | |
return await response.json(); // Return the parsed data | |
} catch (error) { | |
console.error('App Insights API Fetch Error:', error); | |
throw error; | |
} | |
} | |
// --- Event Listeners --- | |
connectionForm.addEventListener('submit', (event) => { | |
event.preventDefault(); | |
const name = connNameInput.value.trim(); | |
const appId = connAppIdInput.value.trim(); | |
const apiKey = connApiKeyInput.value.trim(); | |
if (name && appId && apiKey) { | |
addConnection(name, appId, apiKey); | |
connectionForm.reset(); // Clear form | |
} else { alert('Please fill in all connection fields.'); } | |
}); | |
queryForm.addEventListener('submit', async (event) => { | |
event.preventDefault(); | |
const selectedConnectionId = queryConnectionSelect.value; | |
const kql = queryKqlTextarea.value.trim(); | |
const saveName = querySaveNameInput.value.trim(); | |
if (!selectedConnectionId || !kql) { | |
displayQueryStatus('Please select a connection and enter a KQL query.', true); return; | |
} | |
const connections = getConnections(); | |
const connection = connections.find(c => c.id === selectedConnectionId); | |
if (!connection) { displayQueryStatus('Selected connection not found.', true); return; } | |
runQueryBtn.setAttribute('aria-busy', 'true'); runQueryBtn.disabled = true; | |
queryStatus.textContent = 'Running query...'; queryStatus.className = ''; // Reset status | |
try { | |
const resultsData = await executeAppInsightsQuery(connection.appId, connection.apiKey, kql); | |
displayQueryStatus('Query successful!', false); | |
// Save result if a name was provided | |
if (saveName) { | |
addStoredResult({ | |
name: saveName, | |
query: kql, | |
connectionName: connection.name, // Store connection name for context | |
connectionId: connection.id, // Maybe needed later? | |
timestamp: new Date().toISOString(), | |
data: resultsData // Store the actual data payload | |
}); | |
querySaveNameInput.value = ''; // Clear save name field | |
} else { | |
// Optionally display results immediately if not saved? | |
// renderTable(resultsData); // Or render a temporary view | |
console.log("Query run but result not saved (no name provided)."); | |
} | |
} catch (error) { | |
displayQueryStatus(`Query failed: ${error.message}`, true); | |
} finally { | |
runQueryBtn.setAttribute('aria-busy', 'false'); runQueryBtn.disabled = false; | |
} | |
}); | |
clearResultsBtn.addEventListener('click', clearAllStoredResults); | |
// Visualization Form Logic | |
vizResultSelect.addEventListener('change', (event) => { | |
const selectedResultId = event.target.value; | |
clearVisualizationOutput(); // Clear previous output | |
vizOptionsDiv.style.display = 'none'; // Hide options initially | |
vizTypeSelect.value = 'table'; // Reset viz type | |
chartAxisSelectors.style.display = 'none'; // Hide axis selectors | |
if (selectedResultId) { | |
const results = getStoredResults(); | |
const selectedResult = results.find(r => r.id === selectedResultId); | |
if (selectedResult && selectedResult.data && selectedResult.data.tables && selectedResult.data.tables.length > 0) { | |
const columns = selectedResult.data.tables[0].columns; | |
populateColumnSelectors(columns); | |
vizOptionsDiv.style.display = 'block'; // Show options panel | |
} else { | |
renderErrorInOutput("Selected result has no data or is invalid."); | |
} | |
} | |
}); | |
vizTypeSelect.addEventListener('change', (event) => { | |
const type = event.target.value; | |
if (type === 'table') { | |
chartAxisSelectors.style.display = 'none'; | |
} else { | |
// Repopulate just in case columns weren't ready before | |
const selectedResultId = vizResultSelect.value; | |
if(selectedResultId) { | |
const results = getStoredResults(); | |
const selectedResult = results.find(r => r.id === selectedResultId); | |
if (selectedResult?.data?.tables?.[0]?.columns) { | |
populateColumnSelectors(selectedResult.data.tables[0].columns); // Ensure selectors have columns | |
} | |
} | |
chartAxisSelectors.style.display = 'grid'; // Show axis selectors for charts | |
} | |
clearVisualizationOutput(); // Clear output when type changes | |
}); | |
visualizationForm.addEventListener('submit', (event) => { | |
event.preventDefault(); | |
const selectedResultId = vizResultSelect.value; | |
const vizType = vizTypeSelect.value; // 'table' or 'chart-...' | |
const xAxis = vizXAxisSelect.value; | |
const yAxis = vizYAxisSelect.value; | |
if (!selectedResultId) { | |
renderErrorInOutput("Please select a stored result first."); return; | |
} | |
const results = getStoredResults(); | |
const selectedResult = results.find(r => r.id === selectedResultId); | |
if (!selectedResult || !selectedResult.data) { | |
renderErrorInOutput("Could not find data for the selected result."); return; | |
} | |
if (vizType === 'table') { | |
renderTable(selectedResult.data); | |
} else if (vizType.startsWith('chart-')) { | |
const chartType = vizType.split('-')[1]; // bar, line, pie | |
if (!xAxis || !yAxis) { | |
renderErrorInOutput("Please select columns for both X and Y axes for charts."); return; | |
} | |
renderChart(selectedResult.data, chartType, xAxis, yAxis); | |
} else { | |
renderErrorInOutput("Invalid visualization type selected."); | |
} | |
}); | |
function populateColumnSelectors(columns) { | |
vizXAxisSelect.innerHTML = '<option value="" disabled selected>-- Select --</option>'; | |
vizYAxisSelect.innerHTML = '<option value="" disabled selected>-- Select --</option>'; | |
columns.forEach(col => { | |
const optionX = document.createElement('option'); | |
optionX.value = col.name; | |
optionX.textContent = `${col.name} (${col.type})`; | |
vizXAxisSelect.appendChild(optionX); | |
// Only add numeric types to Y-axis selector? (Optional strictness) | |
// if (['real', 'long', 'int', 'decimal'].includes(col.type.toLowerCase())) { | |
const optionY = document.createElement('option'); | |
optionY.value = col.name; | |
optionY.textContent = `${col.name} (${col.type})`; | |
vizYAxisSelect.appendChild(optionY); | |
// } | |
}); | |
} | |
// --- Initialization --- | |
document.addEventListener('DOMContentLoaded', () => { | |
renderConnections(); | |
renderStoredResults(); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Standalone Azure Application Insights Query & Visualization Tool
This is a single HTML file application that allows you to connect to Azure Application Insights, execute KQL queries, store the results in your browser's
localStorage
, and then create basic tables and charts from that stored data. It requires no backend or build process – just save and open in a browser.Technology:
Core Functionality:
localStorage
.localStorage
.How to Use:
.html
extension (e.g.,appinsights_tool.html
).🚨 CRITICAL SECURITY WARNING 🚨
This tool operates entirely client-side. Your App Insights API Key is entered into the browser, stored in
localStorage
, and sent directly in API requests visible in browser developer tools. This is fundamentally insecure and exposes your API key.Limitations:
localStorage
is browser-specific and can be cleared or exceed size limits (typically 5-10MB).