- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
342 lines
14 KiB
PHP
342 lines
14 KiB
PHP
@extends('admin.layout')
|
|
|
|
@section('title', 'Database Dashboard')
|
|
|
|
@section('content')
|
|
<div class="container-fluid">
|
|
<h1 class="h3 mb-4 text-gray-800">Database Dashboard</h1>
|
|
|
|
<div class="row">
|
|
<!-- Connection Selector -->
|
|
<div class="col-md-12 mb-4">
|
|
<div class="card shadow">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">Database Connection</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<select id="connection-selector" class="form-control">
|
|
@foreach($connections as $conn)
|
|
<option value="{{ $conn }}" {{ $conn === $activeConnection ? 'selected' : '' }}>
|
|
{{ $conn }}
|
|
</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Connection Stats -->
|
|
<div class="col-md-6 mb-4">
|
|
<div class="card shadow">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">Connection Information</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered" id="connection-info">
|
|
<tbody>
|
|
<tr>
|
|
<th>Driver</th>
|
|
<td>{{ $stats['driver'] ?? 'Unknown' }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Version</th>
|
|
<td>{{ $stats['version'] ?? 'Unknown' }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Connection Status</th>
|
|
<td>{{ $stats['connection_status'] ?? 'Unknown' }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Tables</th>
|
|
<td>{{ $stats['table_count'] ?? 'Unknown' }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Indexes</th>
|
|
<td>{{ $stats['index_count'] ?? 'Unknown' }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Total Rows (approx.)</th>
|
|
<td>{{ number_format($stats['total_rows'] ?? 0) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Query Stats -->
|
|
<div class="col-md-6 mb-4">
|
|
<div class="card shadow">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">Query Statistics</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered" id="query-stats">
|
|
<tbody>
|
|
<tr>
|
|
<th>Total Queries</th>
|
|
<td id="total-queries">Loading...</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Total Query Time</th>
|
|
<td id="total-time">Loading...</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Average Query Time</th>
|
|
<td id="average-time">Loading...</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Slow Queries</th>
|
|
<td id="slow-queries">Loading...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Recent Queries -->
|
|
<div class="col-md-12 mb-4">
|
|
<div class="card shadow">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">Recent Queries</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered" id="recent-queries">
|
|
<thead>
|
|
<tr>
|
|
<th>SQL</th>
|
|
<th>Parameters</th>
|
|
<th>Time (ms)</th>
|
|
<th>Timestamp</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td colspan="4" class="text-center">Loading...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Slow Queries -->
|
|
<div class="col-md-12 mb-4">
|
|
<div class="card shadow">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">Slow Queries</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered" id="slow-queries-table">
|
|
<thead>
|
|
<tr>
|
|
<th>SQL</th>
|
|
<th>Parameters</th>
|
|
<th>Time (ms)</th>
|
|
<th>Timestamp</th>
|
|
<th>Connection</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td colspan="5" class="text-center">Loading...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Database-Specific Stats -->
|
|
<div class="col-md-12 mb-4">
|
|
<div class="card shadow">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">Database-Specific Statistics</h6>
|
|
</div>
|
|
<div class="card-body" id="specific-stats">
|
|
<div class="text-center">
|
|
<div class="spinner-border" role="status">
|
|
<span class="sr-only">Loading...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@section('scripts')
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Load initial data
|
|
loadQueryStats();
|
|
loadSpecificStats('{{ $activeConnection }}');
|
|
|
|
// Set up connection selector
|
|
$('#connection-selector').change(function() {
|
|
const connection = $(this).val();
|
|
window.location.href = '/admin/database/dashboard?connection=' + connection;
|
|
});
|
|
|
|
// Auto-refresh data every 10 seconds
|
|
setInterval(function() {
|
|
loadQueryStats();
|
|
loadSpecificStats('{{ $activeConnection }}');
|
|
}, 10000);
|
|
});
|
|
|
|
function loadQueryStats() {
|
|
$.ajax({
|
|
url: '/admin/database/queries',
|
|
method: 'GET',
|
|
success: function(data) {
|
|
// Update query stats
|
|
$('#total-queries').text(data.total_queries);
|
|
$('#total-time').text(data.total_time.toFixed(2) + ' ms');
|
|
$('#average-time').text(data.average_time.toFixed(2) + ' ms');
|
|
$('#slow-queries').text(data.slow_queries);
|
|
|
|
// Update recent queries table
|
|
const recentQueriesHtml = data.recent_queries.map(query => `
|
|
<tr>
|
|
<td><code>${escapeHtml(query.sql)}</code></td>
|
|
<td><code>${JSON.stringify(query.parameters)}</code></td>
|
|
<td>${query.time.toFixed(2)} ms</td>
|
|
<td>${new Date(query.timestamp * 1000).toLocaleString()}</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
$('#recent-queries tbody').html(recentQueriesHtml || '<tr><td colspan="4" class="text-center">No queries found</td></tr>');
|
|
|
|
// Update slow queries table
|
|
const slowQueriesHtml = data.slow_query_list.map(query => `
|
|
<tr>
|
|
<td><code>${escapeHtml(query.sql)}</code></td>
|
|
<td><code>${JSON.stringify(query.parameters)}</code></td>
|
|
<td>${query.time.toFixed(2)} ms</td>
|
|
<td>${new Date(query.timestamp * 1000).toLocaleString()}</td>
|
|
<td>${query.connection}</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
$('#slow-queries-table tbody').html(slowQueriesHtml || '<tr><td colspan="5" class="text-center">No slow queries found</td></tr>');
|
|
},
|
|
error: function() {
|
|
console.error('Failed to load query statistics');
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadSpecificStats(connection) {
|
|
$.ajax({
|
|
url: '/admin/database/specific/' + connection,
|
|
method: 'GET',
|
|
success: function(data) {
|
|
let html = '';
|
|
|
|
// MySQL-specific stats
|
|
if (data.query_cache) {
|
|
html += '<h5>Query Cache</h5>';
|
|
html += '<div class="table-responsive"><table class="table table-bordered">';
|
|
html += '<tbody>';
|
|
for (const [key, value] of Object.entries(data.query_cache)) {
|
|
html += `<tr><th>${key}</th><td>${value}</td></tr>`;
|
|
}
|
|
html += '</tbody></table></div>';
|
|
}
|
|
|
|
if (data.buffer_pool) {
|
|
html += '<h5>Buffer Pool</h5>';
|
|
html += '<div class="table-responsive"><table class="table table-bordered">';
|
|
html += '<tbody>';
|
|
for (const [key, value] of Object.entries(data.buffer_pool)) {
|
|
html += `<tr><th>${key}</th><td>${value}</td></tr>`;
|
|
}
|
|
html += '</tbody></table></div>';
|
|
}
|
|
|
|
// PostgreSQL-specific stats
|
|
if (data.cache) {
|
|
html += '<h5>Cache Statistics</h5>';
|
|
html += '<div class="table-responsive"><table class="table table-bordered">';
|
|
html += '<tbody>';
|
|
for (const [key, value] of Object.entries(data.cache)) {
|
|
html += `<tr><th>${key}</th><td>${value}</td></tr>`;
|
|
}
|
|
html += '</tbody></table></div>';
|
|
}
|
|
|
|
if (data.long_running && data.long_running.length > 0) {
|
|
html += '<h5>Long-Running Queries</h5>';
|
|
html += '<div class="table-responsive"><table class="table table-bordered">';
|
|
html += '<thead><tr><th>Query</th><th>Duration</th><th>User</th></tr></thead>';
|
|
html += '<tbody>';
|
|
for (const query of data.long_running) {
|
|
html += `<tr>
|
|
<td><code>${escapeHtml(query.query)}</code></td>
|
|
<td>${query.duration.toFixed(2)} seconds</td>
|
|
<td>${query.usename}</td>
|
|
</tr>`;
|
|
}
|
|
html += '</tbody></table></div>';
|
|
}
|
|
|
|
// SQLite-specific stats
|
|
if (data.database) {
|
|
html += '<h5>Database Statistics</h5>';
|
|
html += '<div class="table-responsive"><table class="table table-bordered">';
|
|
html += '<tbody>';
|
|
for (const [key, value] of Object.entries(data.database)) {
|
|
html += `<tr><th>${key}</th><td>${value}</td></tr>`;
|
|
}
|
|
html += '</tbody></table></div>';
|
|
}
|
|
|
|
if (data.pragmas) {
|
|
html += '<h5>PRAGMA Settings</h5>';
|
|
html += '<div class="table-responsive"><table class="table table-bordered">';
|
|
html += '<tbody>';
|
|
for (const [key, value] of Object.entries(data.pragmas)) {
|
|
html += `<tr><th>${key}</th><td>${value}</td></tr>`;
|
|
}
|
|
html += '</tbody></table></div>';
|
|
}
|
|
|
|
$('#specific-stats').html(html || '<div class="text-center">No specific statistics available</div>');
|
|
},
|
|
error: function() {
|
|
console.error('Failed to load database-specific statistics');
|
|
$('#specific-stats').html('<div class="alert alert-danger">Failed to load database-specific statistics</div>');
|
|
}
|
|
});
|
|
}
|
|
|
|
function escapeHtml(unsafe) {
|
|
return unsafe
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
</script>
|
|
@endsection
|