537 lines
12 KiB
Vue
537 lines
12 KiB
Vue
<template>
|
|
<div class="database-tab">
|
|
<div class="controls">
|
|
<div class="input-group">
|
|
<label for="queryType">Query Type</label>
|
|
<select id="queryType" v-model="queryType">
|
|
<option value="select">SELECT</option>
|
|
<option value="insert">INSERT</option>
|
|
<option value="update">UPDATE</option>
|
|
<option value="delete">DELETE</option>
|
|
<option value="create_table">CREATE TABLE</option>
|
|
<option value="drop_table">DROP TABLE</option>
|
|
<option value="show_tables">SHOW TABLES</option>
|
|
<option value="describe_table">DESCRIBE TABLE</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="input-group">
|
|
<label for="tableName">Table Name</label>
|
|
<input
|
|
id="tableName"
|
|
v-model="tableName"
|
|
type="text"
|
|
placeholder="Enter table name"
|
|
/>
|
|
</div>
|
|
|
|
<div class="input-group">
|
|
<label for="queryParams">Query Parameters (JSON)</label>
|
|
<textarea
|
|
id="queryParams"
|
|
v-model="queryParams"
|
|
placeholder='{"column": "value", "column2": "value2"}'
|
|
rows="3"
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="input-group">
|
|
<label for="customQuery">Custom SQL Query</label>
|
|
<textarea
|
|
id="customQuery"
|
|
v-model="customQuery"
|
|
placeholder="Enter custom SQL query"
|
|
rows="4"
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<button
|
|
class="btn btn-primary"
|
|
:disabled="!isConnected || (!tableName.trim() && !customQuery.trim())"
|
|
@click="executeQuery"
|
|
>
|
|
Execute Query
|
|
</button>
|
|
|
|
<button
|
|
class="btn btn-secondary"
|
|
:disabled="!isConnected"
|
|
@click="getStats"
|
|
>
|
|
Get Database Stats
|
|
</button>
|
|
|
|
<button
|
|
class="btn btn-secondary"
|
|
:disabled="!isConnected"
|
|
@click="getMonitoringData"
|
|
>
|
|
Get Monitoring Data
|
|
</button>
|
|
|
|
<button class="btn btn-secondary" @click="clearQueryHistory">
|
|
Clear Query History
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Query History -->
|
|
<div class="query-history-section">
|
|
<h3>Query History</h3>
|
|
<div class="query-history">
|
|
<div
|
|
v-for="(query, index) in queryHistory.slice(0, 10)"
|
|
:key="index"
|
|
class="query-item"
|
|
>
|
|
<div class="query-header">
|
|
<div class="query-type">{{ query.type }}</div>
|
|
<div class="query-time">{{ formatTime(query.timestamp) }}</div>
|
|
<div
|
|
class="query-status"
|
|
:class="query.success ? 'success' : 'error'"
|
|
>
|
|
{{ query.success ? "Success" : "Error" }}
|
|
</div>
|
|
</div>
|
|
<div class="query-details">
|
|
<div class="query-sql">{{ query.sql }}</div>
|
|
<div v-if="query.params" class="query-params">
|
|
<strong>Parameters:</strong> {{ query.params }}
|
|
</div>
|
|
<div v-if="query.result" class="query-result">
|
|
<strong>Result:</strong>
|
|
<pre>{{ JSON.stringify(query.result, null, 2) }}</pre>
|
|
</div>
|
|
<div v-if="query.error" class="query-error">
|
|
<strong>Error:</strong> {{ query.error }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="queryHistory.length === 0" class="no-queries">
|
|
No queries executed yet
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Database Stats -->
|
|
<div class="database-stats-section" v-if="stats">
|
|
<h3>Database Statistics</h3>
|
|
<div class="stats-grid">
|
|
<div class="stat-item">
|
|
<div class="stat-label">Connected Clients</div>
|
|
<div class="stat-value">{{ stats.connected_clients }}</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-label">Unique IPs</div>
|
|
<div class="stat-value">{{ stats.unique_ips }}</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-label">Static Clients</div>
|
|
<div class="stat-value">{{ stats.static_clients }}</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-label">Active Rooms</div>
|
|
<div class="stat-value">{{ stats.active_rooms }}</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-label">Message Queue Size</div>
|
|
<div class="stat-value">{{ stats.message_queue_size }}</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-label">Queue Workers</div>
|
|
<div class="stat-value">{{ stats.queue_workers }}</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-label">Uptime</div>
|
|
<div class="stat-value">{{ formatUptime(stats.uptime) }}</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-label">Last Updated</div>
|
|
<div class="stat-value">{{ formatTime(stats.timestamp) }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from "vue";
|
|
import { useWebSocket } from "../../composables/useWebSocket";
|
|
|
|
const {
|
|
isConnected,
|
|
stats,
|
|
executeDatabaseQuery,
|
|
getStats,
|
|
getMonitoringData,
|
|
} = useWebSocket();
|
|
|
|
const queryType = ref("select");
|
|
const tableName = ref("");
|
|
const queryParams = ref("");
|
|
const customQuery = ref("");
|
|
const queryHistory = ref<
|
|
Array<{
|
|
type: string;
|
|
sql: string;
|
|
params: string;
|
|
result: any;
|
|
error: string;
|
|
success: boolean;
|
|
timestamp: number;
|
|
}>
|
|
>([]);
|
|
|
|
const executeQuery = async () => {
|
|
if (!isConnected.value) return;
|
|
|
|
let sql = "";
|
|
let params = {};
|
|
|
|
try {
|
|
if (customQuery.value.trim()) {
|
|
sql = customQuery.value.trim();
|
|
params = queryParams.value ? JSON.parse(queryParams.value) : {};
|
|
} else {
|
|
switch (queryType.value) {
|
|
case "select":
|
|
sql = `SELECT * FROM ${tableName.value}`;
|
|
break;
|
|
case "insert":
|
|
params = JSON.parse(queryParams.value || "{}");
|
|
sql = `INSERT INTO ${tableName.value} SET ?`;
|
|
break;
|
|
case "update":
|
|
params = JSON.parse(queryParams.value || "{}");
|
|
sql = `UPDATE ${tableName.value} SET ? WHERE id = ?`;
|
|
break;
|
|
case "delete":
|
|
sql = `DELETE FROM ${tableName.value} WHERE id = ?`;
|
|
break;
|
|
case "create_table":
|
|
sql = `CREATE TABLE IF NOT EXISTS ${tableName.value} (id INT AUTO_INCREMENT PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`;
|
|
break;
|
|
case "drop_table":
|
|
sql = `DROP TABLE IF EXISTS ${tableName.value}`;
|
|
break;
|
|
case "show_tables":
|
|
sql = "SHOW TABLES";
|
|
break;
|
|
case "describe_table":
|
|
sql = `DESCRIBE ${tableName.value}`;
|
|
break;
|
|
}
|
|
}
|
|
|
|
async function executeDatabaseQuery(queryData: {
|
|
sql: string;
|
|
params?: any;
|
|
table?: string;
|
|
}): Promise<any> {
|
|
// Function implementation
|
|
}
|
|
// Add to history
|
|
queryHistory.value.unshift({
|
|
type: queryType.value,
|
|
sql,
|
|
params: JSON.stringify(params),
|
|
result: await executeDatabaseQuery({
|
|
sql,
|
|
params,
|
|
table: tableName.value,
|
|
}),
|
|
error: "",
|
|
success: true,
|
|
timestamp: Date.now(),
|
|
});
|
|
} catch (error) {
|
|
// Add error to history
|
|
queryHistory.value.unshift({
|
|
type: queryType.value,
|
|
sql: customQuery.value || sql,
|
|
params: queryParams.value,
|
|
result: null,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
success: false,
|
|
timestamp: Date.now(),
|
|
});
|
|
}
|
|
};
|
|
|
|
const clearQueryHistory = () => {
|
|
queryHistory.value = [];
|
|
};
|
|
|
|
const formatTime = (timestamp: number) => {
|
|
return new Date(timestamp).toLocaleString();
|
|
};
|
|
|
|
const formatUptime = (uptime: number) => {
|
|
const seconds = Math.floor(uptime / 1000);
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
const secs = seconds % 60;
|
|
return `${hours.toString().padStart(2, "0")}:${minutes
|
|
.toString()
|
|
.padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.database-tab {
|
|
padding: 20px 0;
|
|
}
|
|
|
|
.controls {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 24px;
|
|
padding: 24px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.input-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.input-group label {
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.input-group input,
|
|
.input-group select,
|
|
.input-group textarea {
|
|
padding: 12px 16px;
|
|
border: 2px solid #e9ecef;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
transition: border-color 0.3s ease;
|
|
}
|
|
|
|
.input-group input:focus,
|
|
.input-group select:focus,
|
|
.input-group textarea:focus {
|
|
outline: none;
|
|
border-color: #1976d2;
|
|
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
|
|
}
|
|
|
|
.btn {
|
|
padding: 12px 24px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
transition: all 0.3s ease;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #1976d2;
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
background: #1565c0;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #6c757d;
|
|
color: white;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #5a6268;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3);
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.query-history-section {
|
|
margin: 32px 0;
|
|
}
|
|
|
|
.query-history-section h3 {
|
|
margin-bottom: 16px;
|
|
color: #333;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.query-history {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
.query-item {
|
|
margin: 16px 0;
|
|
background: white;
|
|
border-radius: 8px;
|
|
border: 1px solid #e9ecef;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.query-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 12px 16px;
|
|
background: #f8f9fa;
|
|
border-bottom: 1px solid #e9ecef;
|
|
}
|
|
|
|
.query-type {
|
|
font-weight: 600;
|
|
color: #1976d2;
|
|
text-transform: uppercase;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.query-time {
|
|
color: #666;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.query-status {
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.query-status.success {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.query-status.error {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
.query-details {
|
|
padding: 16px;
|
|
}
|
|
|
|
.query-sql {
|
|
font-family: monospace;
|
|
background: #f8f9fa;
|
|
padding: 12px;
|
|
border-radius: 4px;
|
|
margin-bottom: 12px;
|
|
font-size: 13px;
|
|
color: #333;
|
|
}
|
|
|
|
.query-params,
|
|
.query-result,
|
|
.query-error {
|
|
margin: 12px 0;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.query-params strong,
|
|
.query-result strong,
|
|
.query-error strong {
|
|
color: #333;
|
|
display: block;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.query-result pre {
|
|
background: #f8f9fa;
|
|
padding: 12px;
|
|
border-radius: 4px;
|
|
overflow-x: auto;
|
|
font-size: 12px;
|
|
color: #333;
|
|
}
|
|
|
|
.query-error {
|
|
color: #dc3545;
|
|
background: #f8d7da;
|
|
padding: 12px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.no-queries {
|
|
text-align: center;
|
|
color: #666;
|
|
font-style: italic;
|
|
padding: 40px;
|
|
}
|
|
|
|
.database-stats-section {
|
|
margin-top: 32px;
|
|
}
|
|
|
|
.database-stats-section h3 {
|
|
margin-bottom: 16px;
|
|
color: #333;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.stat-item {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
border: 1px solid #e9ecef;
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-label {
|
|
color: #666;
|
|
font-size: 14px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.stat-value {
|
|
color: #333;
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 768px) {
|
|
.controls {
|
|
grid-template-columns: 1fr;
|
|
padding: 16px;
|
|
}
|
|
|
|
.query-header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
}
|
|
|
|
.stats-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|