Files
controls-web/controls-classic/test/HMI/query.php
2026-02-17 09:29:34 -06:00

1495 lines
52 KiB
PHP

<?php
// filepath: v:\controls\test\HMI\query_builder.php
include "../../session.php";
include "../../userAccess.php";
// Database connection
$servername = "192.168.0.13";
$username = "opce";
$password = "opcelasuca";
$dbname = "archive";
try {
$pdo = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
if (isset($_POST['action'])) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Database connection failed: ' . $e->getMessage()]);
exit;
}
die("Connection failed: " . $e->getMessage());
}
// Handle AJAX requests
if (isset($_POST['action'])) {
while (ob_get_level()) {
ob_end_clean();
}
ob_start();
header('Content-Type: application/json');
try {
switch ($_POST['action']) {
case 'get_schema':
// Get available tags and their data types
$tagsStmt = $pdo->prepare("SELECT DISTINCT name FROM id_names WHERE name IS NOT NULL AND name != '' ORDER BY name");
$tagsStmt->execute();
$tags = $tagsStmt->fetchAll(PDO::FETCH_COLUMN);
// Get sample data to determine data types and ranges
$schema = [];
foreach (array_slice($tags, 0, 50) as $tag) { // Limit to first 50 tags for performance
$sampleStmt = $pdo->prepare("
SELECT
MIN(h.Value) as min_value,
MAX(h.Value) as max_value,
AVG(h.Value) as avg_value,
COUNT(*) as record_count,
MIN(h.TimeStamp) as earliest_date,
MAX(h.TimeStamp) as latest_date
FROM historicaldata h
INNER JOIN id_names n ON h.ID = n.idnumber
WHERE n.name = ?
AND h.Value IS NOT NULL
AND h.TimeStamp >= DATE_SUB(NOW(), INTERVAL 30 DAY)
");
$sampleStmt->execute([$tag]);
$stats = $sampleStmt->fetch(PDO::FETCH_ASSOC);
if ($stats && $stats['record_count'] > 0) {
$schema[] = [
'name' => $tag,
'type' => 'numeric',
'min_value' => (float)$stats['min_value'],
'max_value' => (float)$stats['max_value'],
'avg_value' => (float)$stats['avg_value'],
'record_count' => (int)$stats['record_count'],
'earliest_date' => $stats['earliest_date'],
'latest_date' => $stats['latest_date']
];
}
}
echo json_encode([
'success' => true,
'schema' => $schema,
'total_tags' => count($tags)
]);
break;
case 'execute_query':
$queryConfig = json_decode($_POST['query_config'] ?? '{}', true);
if (empty($queryConfig) || !isset($queryConfig['conditions'])) {
throw new Exception('Invalid query configuration');
}
// Build SQL query from configuration
$sql = $this->buildSQLFromConfig($queryConfig, $pdo);
// Execute query with limit for safety
$limit = min((int)($queryConfig['limit'] ?? 1000), 10000);
$sql .= " LIMIT " . $limit;
$stmt = $pdo->prepare($sql['query']);
$stmt->execute($sql['params']);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'data' => $results,
'row_count' => count($results),
'sql_query' => $sql['query'],
'parameters' => $sql['params'],
'execution_time' => microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']
]);
break;
case 'save_query':
$queryName = $_POST['query_name'] ?? '';
$queryConfig = $_POST['query_config'] ?? '';
$userId = $_SESSION['user_id'] ?? 'anonymous';
if (empty($queryName) || empty($queryConfig)) {
throw new Exception('Query name and configuration required');
}
// Create saved queries table if it doesn't exist
$pdo->exec("CREATE TABLE IF NOT EXISTS saved_queries (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(50),
query_name VARCHAR(100),
query_config TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)");
$stmt = $pdo->prepare("INSERT INTO saved_queries (user_id, query_name, query_config) VALUES (?, ?, ?)");
$stmt->execute([$userId, $queryName, $queryConfig]);
echo json_encode([
'success' => true,
'query_id' => $pdo->lastInsertId(),
'message' => 'Query saved successfully'
]);
break;
case 'load_saved_queries':
$userId = $_SESSION['user_id'] ?? 'anonymous';
$stmt = $pdo->prepare("SELECT id, query_name, query_config, created_at FROM saved_queries WHERE user_id = ? ORDER BY created_at DESC");
$stmt->execute([$userId]);
$queries = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'saved_queries' => $queries
]);
break;
case 'get_field_values':
$fieldName = $_POST['field_name'] ?? '';
if (empty($fieldName)) {
throw new Exception('Field name required');
}
// Get distinct values for the field (limited for performance)
$stmt = $pdo->prepare("
SELECT DISTINCT h.Value
FROM historicaldata h
INNER JOIN id_names n ON h.ID = n.idnumber
WHERE n.name = ?
AND h.Value IS NOT NULL
AND h.TimeStamp >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY h.Value
LIMIT 100
");
$stmt->execute([$fieldName]);
$values = $stmt->fetchAll(PDO::FETCH_COLUMN);
echo json_encode([
'success' => true,
'field_name' => $fieldName,
'values' => array_map('floatval', $values)
]);
break;
default:
throw new Exception('Invalid action specified');
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
} catch (Error $e) {
echo json_encode(['success' => false, 'error' => 'PHP Error: ' . $e->getMessage()]);
}
ob_end_flush();
exit;
}
// Helper function to build SQL from query configuration
function buildSQLFromConfig($config, $pdo) {
$selectedTags = $config['selected_tags'] ?? [];
$conditions = $config['conditions'] ?? [];
$orderBy = $config['order_by'] ?? [];
$groupBy = $config['group_by'] ?? [];
$aggregations = $config['aggregations'] ?? [];
if (empty($selectedTags)) {
throw new Exception('No tags selected');
}
// Build SELECT clause
$selectFields = [];
$selectFields[] = "h.TimeStamp";
if (!empty($aggregations)) {
// Aggregation query
foreach ($selectedTags as $tag) {
foreach ($aggregations as $agg) {
$selectFields[] = "{$agg['function']}(CASE WHEN n.name = '{$tag}' THEN h.Value END) as {$tag}_{$agg['function']}";
}
}
} else {
// Regular query
$selectFields[] = "n.name as tag_name";
$selectFields[] = "h.Value";
}
$sql = "SELECT " . implode(', ', $selectFields) . " FROM historicaldata h INNER JOIN id_names n ON h.ID = n.idnumber";
// Build WHERE clause
$whereConditions = [];
$params = [];
// Tag filter
$tagPlaceholders = str_repeat('?,', count($selectedTags) - 1) . '?';
$whereConditions[] = "n.name IN ($tagPlaceholders)";
$params = array_merge($params, $selectedTags);
// Custom conditions
foreach ($conditions as $condition) {
if (empty($condition['field']) || empty($condition['operator'])) continue;
$field = $condition['field'];
$operator = $condition['operator'];
$value = $condition['value'];
$logicalOp = $condition['logical'] ?? 'AND';
$conditionSql = '';
switch ($operator) {
case 'equals':
if ($field === 'TimeStamp') {
$conditionSql = "h.TimeStamp = ?";
} else {
$conditionSql = "(n.name = ? AND h.Value = ?)";
$params[] = $field;
}
$params[] = $value;
break;
case 'not_equals':
if ($field === 'TimeStamp') {
$conditionSql = "h.TimeStamp != ?";
} else {
$conditionSql = "(n.name = ? AND h.Value != ?)";
$params[] = $field;
}
$params[] = $value;
break;
case 'greater_than':
if ($field === 'TimeStamp') {
$conditionSql = "h.TimeStamp > ?";
} else {
$conditionSql = "(n.name = ? AND h.Value > ?)";
$params[] = $field;
}
$params[] = $value;
break;
case 'less_than':
if ($field === 'TimeStamp') {
$conditionSql = "h.TimeStamp < ?";
} else {
$conditionSql = "(n.name = ? AND h.Value < ?)";
$params[] = $field;
}
$params[] = $value;
break;
case 'between':
$values = explode(',', $value);
if (count($values) === 2) {
if ($field === 'TimeStamp') {
$conditionSql = "h.TimeStamp BETWEEN ? AND ?";
} else {
$conditionSql = "(n.name = ? AND h.Value BETWEEN ? AND ?)";
$params[] = $field;
}
$params[] = trim($values[0]);
$params[] = trim($values[1]);
}
break;
case 'in':
$values = explode(',', $value);
$valuePlaceholders = str_repeat('?,', count($values) - 1) . '?';
if ($field === 'TimeStamp') {
$conditionSql = "h.TimeStamp IN ($valuePlaceholders)";
} else {
$conditionSql = "(n.name = ? AND h.Value IN ($valuePlaceholders))";
$params[] = $field;
}
foreach ($values as $val) {
$params[] = trim($val);
}
break;
}
if ($conditionSql) {
if (!empty($whereConditions) && count($whereConditions) > 1) {
$whereConditions[] = $logicalOp . ' ' . $conditionSql;
} else {
$whereConditions[] = $conditionSql;
}
}
}
if (!empty($whereConditions)) {
$sql .= " WHERE " . implode(' ', $whereConditions);
}
// Add GROUP BY
if (!empty($groupBy)) {
$sql .= " GROUP BY " . implode(', ', $groupBy);
}
// Add ORDER BY
if (!empty($orderBy)) {
$orderClauses = [];
foreach ($orderBy as $order) {
$orderClauses[] = $order['field'] . ' ' . ($order['direction'] ?? 'ASC');
}
$sql .= " ORDER BY " . implode(', ', $orderClauses);
} else {
$sql .= " ORDER BY h.TimeStamp DESC";
}
return ['query' => $sql, 'params' => $params];
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LASUCA Controls - Advanced Query Builder</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
min-height: 100vh;
}
.dashboard-header {
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
padding: 20px;
text-align: center;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
}
.dashboard-header h1 {
color: #fff;
font-size: 2.5rem;
margin: 0;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
.dashboard-header .subtitle {
color: #ffd700;
font-size: 1.2rem;
margin-top: 10px;
font-weight: 400;
}
.container {
max-width: 1800px;
margin: 0 auto;
padding: 20px;
}
.query-builder-layout {
display: grid;
grid-template-columns: 350px 1fr;
gap: 20px;
height: calc(100vh - 120px);
}
/* Left Sidebar */
.sidebar {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(15px);
border-radius: 20px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
overflow-y: auto;
}
.sidebar-section {
margin-bottom: 25px;
}
.sidebar-section h3 {
color: #ffd700;
font-size: 1.1rem;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 8px;
}
/* Field Library */
.field-library {
display: grid;
gap: 8px;
}
.field-item {
background: rgba(255, 255, 255, 0.15);
padding: 12px;
border-radius: 10px;
cursor: grab;
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.1);
user-select: none;
}
.field-item:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
}
.field-item:active {
cursor: grabbing;
}
.field-name {
font-weight: 600;
color: #fff;
font-size: 0.9rem;
}
.field-info {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.7);
margin-top: 4px;
}
/* Saved Queries */
.saved-queries {
max-height: 200px;
overflow-y: auto;
}
.saved-query-item {
background: rgba(72, 187, 120, 0.2);
padding: 10px;
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid rgba(72, 187, 120, 0.3);
}
.saved-query-item:hover {
background: rgba(72, 187, 120, 0.3);
}
.query-name {
font-weight: 600;
font-size: 0.9rem;
}
.query-date {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.7);
}
/* Main Query Builder */
.main-content {
display: grid;
grid-template-rows: auto 1fr auto;
gap: 20px;
}
/* Query Configuration Panel */
.query-config {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(15px);
border-radius: 20px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.config-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tab-button {
background: rgba(255, 255, 255, 0.1);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
}
.tab-button.active {
background: rgba(255, 255, 255, 0.3);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Drag & Drop Areas */
.drop-zone {
min-height: 120px;
border: 2px dashed rgba(255, 255, 255, 0.3);
border-radius: 15px;
padding: 20px;
margin-bottom: 15px;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.05);
}
.drop-zone.drag-over {
border-color: #ffd700;
background: rgba(255, 215, 0, 0.1);
transform: scale(1.02);
}
.drop-zone h4 {
color: #ffd700;
margin-bottom: 10px;
font-size: 1rem;
}
.drop-zone-hint {
color: rgba(255, 255, 255, 0.6);
font-style: italic;
text-align: center;
padding: 20px;
}
/* Condition Builder */
.condition-item {
background: rgba(255, 255, 255, 0.15);
border-radius: 10px;
padding: 15px;
margin-bottom: 10px;
display: grid;
grid-template-columns: 100px 1fr 120px 1fr 80px;
gap: 10px;
align-items: center;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.condition-item select,
.condition-item input {
background: rgba(255, 255, 255, 0.1);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
padding: 8px;
font-size: 0.9rem;
}
.condition-item select:focus,
.condition-item input:focus {
outline: none;
border-color: #ffd700;
box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.2);
}
.remove-condition {
background: rgba(239, 68, 68, 0.7);
color: white;
border: none;
border-radius: 6px;
padding: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.remove-condition:hover {
background: rgba(239, 68, 68, 0.9);
}
/* Results Panel */
.results-panel {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(15px);
border-radius: 20px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
overflow: hidden;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.results-title {
color: #ffd700;
font-size: 1.3rem;
font-weight: 600;
}
.results-actions {
display: flex;
gap: 10px;
}
/* Data Table */
.data-table-container {
height: 400px;
overflow: auto;
border-radius: 10px;
background: rgba(255, 255, 255, 0.05);
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
background: rgba(255, 255, 255, 0.2);
color: #fff;
padding: 12px 15px;
text-align: left;
font-weight: 600;
position: sticky;
top: 0;
z-index: 10;
}
.data-table td {
padding: 10px 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.9);
}
.data-table tr:hover {
background: rgba(255, 255, 255, 0.1);
}
/* Control Buttons */
.control-panel {
display: flex;
gap: 15px;
justify-content: center;
margin-bottom: 20px;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 10px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
button:disabled {
background: rgba(255, 255, 255, 0.2);
cursor: not-allowed;
transform: none;
}
button.execute-btn {
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
}
button.save-btn {
background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%);
}
button.export-btn {
background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%);
}
button.clear-btn {
background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
}
/* Loading and Status */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(5px);
display: none;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-spinner {
width: 60px;
height: 60px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid #ffd700;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.status-message {
padding: 15px 20px;
margin-bottom: 20px;
border-radius: 10px;
font-weight: 500;
text-align: center;
}
.status-message.success {
background: rgba(72, 187, 120, 0.2);
border: 1px solid rgba(72, 187, 120, 0.5);
color: #48bb78;
}
.status-message.error {
background: rgba(245, 101, 101, 0.2);
border: 1px solid rgba(245, 101, 101, 0.5);
color: #f56565;
}
.status-message.info {
background: rgba(66, 153, 225, 0.2);
border: 1px solid rgba(66, 153, 225, 0.5);
color: #4299e1;
}
/* Responsive Design */
@media (max-width: 1200px) {
.query-builder-layout {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.sidebar {
max-height: 300px;
}
}
@media (max-width: 768px) {
.condition-item {
grid-template-columns: 1fr;
gap: 8px;
}
.results-header {
flex-direction: column;
gap: 15px;
text-align: center;
}
}
/* Custom Scrollbars */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
</style>
</head>
<body>
<!-- Loading Overlay -->
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-spinner"></div>
</div>
<!-- Header -->
<div class="dashboard-header">
<h1>🎯 LASUCA Controls - Advanced Query Builder</h1>
<p class="subtitle">Visual Data Filtering & Analysis Tool</p>
</div>
<div class="container">
<!-- Status Messages -->
<div id="statusMessages"></div>
<div class="query-builder-layout">
<!-- Left Sidebar -->
<div class="sidebar">
<!-- Field Library -->
<div class="sidebar-section">
<h3>📁 Available Fields</h3>
<div class="field-library" id="fieldLibrary">
<div style="text-align: center; padding: 20px; color: rgba(255,255,255,0.6);">
Loading schema...
</div>
</div>
</div>
<!-- Quick Filters -->
<div class="sidebar-section">
<h3>⚡ Quick Filters</h3>
<div class="control-panel" style="flex-direction: column; gap: 8px;">
<button onclick="addTimeRangeFilter('last_hour')">Last Hour</button>
<button onclick="addTimeRangeFilter('last_24h')">Last 24 Hours</button>
<button onclick="addTimeRangeFilter('last_week')">Last Week</button>
<button onclick="addValueRangeFilter()">Value Range</button>
</div>
</div>
<!-- Saved Queries -->
<div class="sidebar-section">
<h3>💾 Saved Queries</h3>
<div class="saved-queries" id="savedQueries">
<div style="text-align: center; padding: 15px; color: rgba(255,255,255,0.6);">
No saved queries
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="main-content">
<!-- Query Configuration -->
<div class="query-config">
<div class="config-tabs">
<button class="tab-button active" onclick="switchTab('fields')">📊 Fields</button>
<button class="tab-button" onclick="switchTab('conditions')">🔍 Conditions</button>
<button class="tab-button" onclick="switchTab('grouping')">📈 Grouping</button>
<button class="tab-button" onclick="switchTab('sorting')">🔄 Sorting</button>
</div>
<!-- Fields Tab -->
<div class="tab-content active" id="fieldsTab">
<div class="drop-zone" id="selectedFieldsZone" ondrop="dropField(event)" ondragover="allowDrop(event)">
<h4>Selected Fields</h4>
<div class="drop-zone-hint">Drag fields here to include in query results</div>
</div>
</div>
<!-- Conditions Tab -->
<div class="tab-content" id="conditionsTab">
<div class="drop-zone" id="conditionsZone">
<h4>Filter Conditions</h4>
<div id="conditionsList">
<div class="drop-zone-hint">Add conditions to filter your data</div>
</div>
<button onclick="addCondition()" style="margin-top: 10px;">+ Add Condition</button>
</div>
</div>
<!-- Grouping Tab -->
<div class="tab-content" id="groupingTab">
<div class="drop-zone" id="groupingZone" ondrop="dropGroupField(event)" ondragover="allowDrop(event)">
<h4>Group By Fields</h4>
<div class="drop-zone-hint">Drag fields here to group results</div>
</div>
<div class="drop-zone" id="aggregationZone" style="margin-top: 15px;">
<h4>Aggregations</h4>
<div id="aggregationsList">
<div class="drop-zone-hint">Define aggregation functions</div>
</div>
<button onclick="addAggregation()" style="margin-top: 10px;">+ Add Aggregation</button>
</div>
</div>
<!-- Sorting Tab -->
<div class="tab-content" id="sortingTab">
<div class="drop-zone" id="sortingZone" ondrop="dropSortField(event)" ondragover="allowDrop(event)">
<h4>Sort Fields</h4>
<div class="drop-zone-hint">Drag fields here to define sort order</div>
</div>
</div>
</div>
<!-- Control Panel -->
<div class="control-panel">
<button onclick="executeQuery()" class="execute-btn" id="executeBtn">
▶️ Execute Query
</button>
<button onclick="saveQuery()" class="save-btn">
💾 Save Query
</button>
<button onclick="exportResults('csv')" class="export-btn" id="exportBtn" disabled>
📄 Export CSV
</button>
<button onclick="clearQuery()" class="clear-btn">
🗑️ Clear All
</button>
</div>
<!-- Results Panel -->
<div class="results-panel">
<div class="results-header">
<div class="results-title">Query Results</div>
<div class="results-actions">
<span id="rowCount" style="color: rgba(255,255,255,0.8);">No results</span>
</div>
</div>
<div class="data-table-container" id="dataTableContainer">
<div style="text-align: center; padding: 50px; color: rgba(255,255,255,0.6);">
Execute a query to see results here
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Global variables
let schema = [];
let selectedFields = [];
let conditions = [];
let groupByFields = [];
let aggregations = [];
let sortFields = [];
let queryResults = [];
// Initialize the application
document.addEventListener('DOMContentLoaded', function() {
loadSchema();
loadSavedQueries();
setupDragAndDrop();
});
// Load database schema
async function loadSchema() {
try {
showLoading(true);
const formData = new FormData();
formData.append('action', 'get_schema');
const response = await fetch(window.location.href, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
schema = data.schema;
renderFieldLibrary();
showStatus(`${schema.length} fields loaded from ${data.total_tags} available tags`, 'success');
} else {
throw new Error(data.error || 'Failed to load schema');
}
} catch (error) {
console.error('Error loading schema:', error);
showStatus('Failed to load database schema: ' + error.message, 'error');
} finally {
showLoading(false);
}
}
// Render field library
function renderFieldLibrary() {
const library = document.getElementById('fieldLibrary');
if (schema.length === 0) {
library.innerHTML = '<div style="text-align: center; padding: 20px; color: rgba(255,255,255,0.6);">No fields available</div>';
return;
}
let html = '';
// Add special timestamp field
html += `
<div class="field-item" draggable="true" data-field="TimeStamp" data-type="datetime">
<div class="field-name">📅 TimeStamp</div>
<div class="field-info">DateTime field</div>
</div>
`;
// Add schema fields
schema.forEach((field, index) => {
html += `
<div class="field-item" draggable="true" data-field="${field.name}" data-type="${field.type}">
<div class="field-name">📊 ${field.name}</div>
<div class="field-info">
${field.type} • ${field.record_count} records<br>
Range: ${field.min_value?.toFixed(2)} - ${field.max_value?.toFixed(2)}
</div>
</div>
`;
});
library.innerHTML = html;
}
// Setup drag and drop functionality
function setupDragAndDrop() {
// Add event listeners for drag start
document.addEventListener('dragstart', function(e) {
if (e.target.classList.contains('field-item')) {
e.dataTransfer.setData('text/plain', JSON.stringify({
field: e.target.dataset.field,
type: e.target.dataset.type
}));
}
});
// Make selected fields sortable
new Sortable(document.getElementById('selectedFieldsZone'), {
group: 'fields',
animation: 150,
onAdd: function(evt) {
const fieldData = JSON.parse(evt.item.dataset.fieldData || '{}');
if (fieldData.field && !selectedFields.includes(fieldData.field)) {
selectedFields.push(fieldData.field);
updateSelectedFieldsDisplay();
}
evt.item.remove();
}
});
}
// Allow drop
function allowDrop(ev) {
ev.preventDefault();
ev.currentTarget.classList.add('drag-over');
}
// Handle field drop
function dropField(ev) {
ev.preventDefault();
ev.currentTarget.classList.remove('drag-over');
try {
const fieldData = JSON.parse(ev.dataTransfer.getData('text/plain'));
if (fieldData.field && !selectedFields.includes(fieldData.field)) {
selectedFields.push(fieldData.field);
updateSelectedFieldsDisplay();
}
} catch (error) {
console.error('Error parsing dropped field data:', error);
}
}
// Update selected fields display
function updateSelectedFieldsDisplay() {
const zone = document.getElementById('selectedFieldsZone');
if (selectedFields.length === 0) {
zone.innerHTML = `
<h4>Selected Fields</h4>
<div class="drop-zone-hint">Drag fields here to include in query results</div>
`;
return;
}
let html = '<h4>Selected Fields</h4>';
selectedFields.forEach((field, index) => {
html += `
<div class="condition-item" style="grid-template-columns: 1fr auto;">
<span>📊 ${field}</span>
<button class="remove-condition" onclick="removeSelectedField(${index})">✕</button>
</div>
`;
});
zone.innerHTML = html;
}
// Remove selected field
function removeSelectedField(index) {
selectedFields.splice(index, 1);
updateSelectedFieldsDisplay();
}
// Tab switching
function switchTab(tabName) {
// Update tab buttons
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
document.getElementById(tabName + 'Tab').classList.add('active');
}
// Add condition
function addCondition() {
const conditionId = conditions.length;
const conditionHtml = `
<div class="condition-item" id="condition-${conditionId}">
<select onchange="updateCondition(${conditionId}, 'logical', this.value)">
<option value="AND">AND</option>
<option value="OR">OR</option>
</select>
<select onchange="updateCondition(${conditionId}, 'field', this.value)">
<option value="">Select Field</option>
<option value="TimeStamp">TimeStamp</option>
${schema.map(field => `<option value="${field.name}">${field.name}</option>`).join('')}
</select>
<select onchange="updateCondition(${conditionId}, 'operator', this.value)">
<option value="equals">Equals</option>
<option value="not_equals">Not Equals</option>
<option value="greater_than">Greater Than</option>
<option value="less_than">Less Than</option>
<option value="between">Between</option>
<option value="in">In List</option>
</select>
<input type="text" placeholder="Value" onchange="updateCondition(${conditionId}, 'value', this.value)">
<button class="remove-condition" onclick="removeCondition(${conditionId})">✕</button>
</div>
`;
const conditionsContainer = document.getElementById('conditionsList');
if (conditionsContainer.querySelector('.drop-zone-hint')) {
conditionsContainer.innerHTML = '';
}
conditionsContainer.insertAdjacentHTML('beforeend', conditionHtml);
conditions.push({
id: conditionId,
logical: 'AND',
field: '',
operator: 'equals',
value: ''
});
}
// Update condition
function updateCondition(id, property, value) {
const condition = conditions.find(c => c.id === id);
if (condition) {
condition[property] = value;
}
}
// Remove condition
function removeCondition(id) {
document.getElementById(`condition-${id}`).remove();
conditions = conditions.filter(c => c.id !== id);
if (conditions.length === 0) {
document.getElementById('conditionsList').innerHTML =
'<div class="drop-zone-hint">Add conditions to filter your data</div>';
}
}
// Quick filter functions
function addTimeRangeFilter(range) {
let value = '';
switch (range) {
case 'last_hour':
value = new Date(Date.now() - 60 * 60 * 1000).toISOString().slice(0, 19).replace('T', ' ');
break;
case 'last_24h':
value = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().slice(0, 19).replace('T', ' ');
break;
case 'last_week':
value = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 19).replace('T', ' ');
break;
}
addCondition();
const latestCondition = conditions[conditions.length - 1];
latestCondition.field = 'TimeStamp';
latestCondition.operator = 'greater_than';
latestCondition.value = value;
// Update the UI
const conditionElement = document.getElementById(`condition-${latestCondition.id}`);
conditionElement.querySelector('select:nth-child(2)').value = 'TimeStamp';
conditionElement.querySelector('select:nth-child(3)').value = 'greater_than';
conditionElement.querySelector('input').value = value;
showStatus(`Added ${range.replace('_', ' ')} filter`, 'info');
}
// Execute query
async function executeQuery() {
try {
if (selectedFields.length === 0) {
showStatus('Please select at least one field', 'error');
return;
}
showLoading(true);
document.getElementById('executeBtn').disabled = true;
const queryConfig = {
selected_tags: selectedFields,
conditions: conditions.filter(c => c.field && c.operator && c.value),
group_by: groupByFields,
aggregations: aggregations,
order_by: sortFields,
limit: 1000
};
const formData = new FormData();
formData.append('action', 'execute_query');
formData.append('query_config', JSON.stringify(queryConfig));
const response = await fetch(window.location.href, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
queryResults = result.data;
displayResults(result.data);
document.getElementById('exportBtn').disabled = false;
showStatus(`Query executed successfully! Retrieved ${result.row_count} rows in ${(result.execution_time * 1000).toFixed(2)}ms`, 'success');
} else {
throw new Error(result.error || 'Query execution failed');
}
} catch (error) {
console.error('Error executing query:', error);
showStatus('Failed to execute query: ' + error.message, 'error');
} finally {
showLoading(false);
document.getElementById('executeBtn').disabled = false;
}
}
// Display query results
function displayResults(data) {
const container = document.getElementById('dataTableContainer');
const rowCount = document.getElementById('rowCount');
rowCount.textContent = `${data.length} rows`;
if (data.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 50px; color: rgba(255,255,255,0.6);">No data found</div>';
return;
}
// Get column names
const columns = Object.keys(data[0]);
let html = '<table class="data-table"><thead><tr>';
columns.forEach(column => {
html += `<th>${column}</th>`;
});
html += '</tr></thead><tbody>';
data.forEach(row => {
html += '<tr>';
columns.forEach(column => {
let value = row[column];
if (value === null || value === undefined) {
value = '<span style="color: rgba(255,255,255,0.5);">NULL</span>';
} else if (typeof value === 'number') {
value = value.toFixed(2);
}
html += `<td>${value}</td>`;
});
html += '</tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
}
// Save query
async function saveQuery() {
const queryName = prompt('Enter a name for this query:');
if (!queryName) return;
try {
const queryConfig = {
selected_tags: selectedFields,
conditions: conditions.filter(c => c.field && c.operator && c.value),
group_by: groupByFields,
aggregations: aggregations,
order_by: sortFields
};
const formData = new FormData();
formData.append('action', 'save_query');
formData.append('query_name', queryName);
formData.append('query_config', JSON.stringify(queryConfig));
const response = await fetch(window.location.href, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
showStatus('Query saved successfully!', 'success');
loadSavedQueries();
} else {
throw new Error(result.error || 'Failed to save query');
}
} catch (error) {
console.error('Error saving query:', error);
showStatus('Failed to save query: ' + error.message, 'error');
}
}
// Load saved queries
async function loadSavedQueries() {
try {
const formData = new FormData();
formData.append('action', 'load_saved_queries');
const response = await fetch(window.location.href, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
displaySavedQueries(result.saved_queries);
}
} catch (error) {
console.error('Error loading saved queries:', error);
}
}
// Display saved queries
function displaySavedQueries(queries) {
const container = document.getElementById('savedQueries');
if (queries.length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 15px; color: rgba(255,255,255,0.6);">No saved queries</div>';
return;
}
let html = '';
queries.forEach(query => {
html += `
<div class="saved-query-item" onclick="loadQuery('${query.id}')">
<div class="query-name">${query.query_name}</div>
<div class="query-date">${new Date(query.created_at).toLocaleDateString()}</div>
</div>
`;
});
container.innerHTML = html;
}
// Export results
function exportResults(format) {
if (queryResults.length === 0) {
showStatus('No data to export', 'error');
return;
}
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
if (format === 'csv') {
const columns = Object.keys(queryResults[0]);
let csv = columns.join(',') + '\n';
queryResults.forEach(row => {
const values = columns.map(col => {
let value = row[col];
if (value === null || value === undefined) return '';
if (typeof value === 'string' && value.includes(',')) {
return `"${value}"`;
}
return value;
});
csv += values.join(',') + '\n';
});
downloadFile(csv, `query_results_${timestamp}.csv`, 'text/csv');
showStatus('CSV export completed', 'success');
}
}
// Clear query
function clearQuery() {
selectedFields = [];
conditions = [];
groupByFields = [];
aggregations = [];
sortFields = [];
queryResults = [];
updateSelectedFieldsDisplay();
document.getElementById('conditionsList').innerHTML =
'<div class="drop-zone-hint">Add conditions to filter your data</div>';
document.getElementById('dataTableContainer').innerHTML =
'<div style="text-align: center; padding: 50px; color: rgba(255,255,255,0.6);">Execute a query to see results here</div>';
document.getElementById('rowCount').textContent = 'No results';
document.getElementById('exportBtn').disabled = true;
showStatus('Query cleared', 'info');
}
// Utility functions
function downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
}
function showLoading(show) {
document.getElementById('loadingOverlay').style.display = show ? 'flex' : 'none';
}
function showStatus(message, type) {
const container = document.getElementById('statusMessages');
const div = document.createElement('div');
div.className = `status-message ${type}`;
div.textContent = message;
container.innerHTML = '';
container.appendChild(div);
if (type === 'success' || type === 'info') {
setTimeout(() => {
if (div.parentNode === container) {
container.removeChild(div);
}
}, 5000);
}
}
// Remove drag over effect when leaving drop zone
document.addEventListener('dragleave', function(e) {
if (e.target.classList.contains('drop-zone')) {
e.target.classList.remove('drag-over');
}
});
</script>
</body>
</html>