Files
antrean-anjungan/examples/clientsocket/components/tabs/DatabaseTab.vue
2025-09-23 18:47:16 +07:00

529 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
}
}
const result = await executeDatabaseQuery(queryType.value, {
sql,
params,
table: tableName.value
})
// Add to history
queryHistory.value.unshift({
type: queryType.value,
sql,
params: JSON.stringify(params),
result: result,
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>