666 lines
15 KiB
Vue
666 lines
15 KiB
Vue
<template>
|
||
<div class="admin-tab">
|
||
<div class="controls">
|
||
<div class="input-group">
|
||
<label for="adminCommand">Admin Command</label>
|
||
<select id="adminCommand" v-model="adminCommand">
|
||
<option value="restart_server">Restart Server</option>
|
||
<option value="shutdown_server">Shutdown Server</option>
|
||
<option value="clear_cache">Clear Cache</option>
|
||
<option value="reload_config">Reload Configuration</option>
|
||
<option value="backup_database">Backup Database</option>
|
||
<option value="restore_database">Restore Database</option>
|
||
<option value="export_logs">Export Logs</option>
|
||
<option value="clear_logs">Clear Logs</option>
|
||
<option value="update_permissions">Update Permissions</option>
|
||
<option value="custom">Custom Command</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="input-group">
|
||
<label for="commandParams">Command Parameters (JSON)</label>
|
||
<textarea
|
||
id="commandParams"
|
||
v-model="commandParams"
|
||
placeholder='{"target": "all", "force": false}'
|
||
rows="3"
|
||
></textarea>
|
||
</div>
|
||
|
||
<div class="input-group">
|
||
<label for="customCommand">Custom Command</label>
|
||
<input
|
||
id="customCommand"
|
||
v-model="customCommand"
|
||
type="text"
|
||
placeholder="custom_admin_command"
|
||
:disabled="adminCommand !== 'custom'"
|
||
/>
|
||
</div>
|
||
|
||
<div class="input-group">
|
||
<label for="confirmationCode">Confirmation Code</label>
|
||
<input
|
||
id="confirmationCode"
|
||
v-model="confirmationCode"
|
||
type="text"
|
||
placeholder="Enter confirmation code"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<button
|
||
class="btn btn-danger"
|
||
:disabled="!isConnected || !confirmationCode.trim()"
|
||
@click="executeAdminCommand"
|
||
>
|
||
Execute Command
|
||
</button>
|
||
|
||
<button
|
||
class="btn btn-secondary"
|
||
:disabled="!isConnected"
|
||
@click="getServerInfo"
|
||
>
|
||
Get Server Info
|
||
</button>
|
||
|
||
<button
|
||
class="btn btn-secondary"
|
||
:disabled="!isConnected"
|
||
@click="getSystemHealth"
|
||
>
|
||
Get System Health
|
||
</button>
|
||
|
||
<button
|
||
class="btn btn-warning"
|
||
@click="clearCommandHistory"
|
||
>
|
||
Clear History
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Server Information -->
|
||
<div class="server-info-section" v-if="serverInfo">
|
||
<h3>Server Information</h3>
|
||
<div class="info-grid">
|
||
<div class="info-item">
|
||
<div class="info-label">Server Version</div>
|
||
<div class="info-value">{{ serverInfo.version }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">Environment</div>
|
||
<div class="info-value">{{ serverInfo.environment }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">Node Version</div>
|
||
<div class="info-value">{{ serverInfo.nodeVersion }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">Platform</div>
|
||
<div class="info-value">{{ serverInfo.platform }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">Architecture</div>
|
||
<div class="info-value">{{ serverInfo.architecture }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">CPU Cores</div>
|
||
<div class="info-value">{{ serverInfo.cpuCores }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">Memory Usage</div>
|
||
<div class="info-value">{{ formatBytes(serverInfo.memoryUsage) }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">Uptime</div>
|
||
<div class="info-value">{{ formatUptime(serverInfo.uptime) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- System Health -->
|
||
<div class="system-health-section" v-if="systemHealth">
|
||
<h3>System Health</h3>
|
||
<div class="health-grid">
|
||
<div class="health-item">
|
||
<div class="health-label">CPU Usage</div>
|
||
<div class="health-value">{{ systemHealth.cpuUsage }}%</div>
|
||
<div class="health-bar">
|
||
<div
|
||
class="health-bar-fill"
|
||
:style="{ width: systemHealth.cpuUsage + '%' }"
|
||
:class="getCpuUsageClass(systemHealth.cpuUsage)"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
<div class="health-item">
|
||
<div class="health-label">Memory Usage</div>
|
||
<div class="health-value">{{ systemHealth.memoryUsage }}%</div>
|
||
<div class="health-bar">
|
||
<div
|
||
class="health-bar-fill"
|
||
:style="{ width: systemHealth.memoryUsage + '%' }"
|
||
:class="getMemoryUsageClass(systemHealth.memoryUsage)"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
<div class="health-item">
|
||
<div class="health-label">Disk Usage</div>
|
||
<div class="health-value">{{ systemHealth.diskUsage }}%</div>
|
||
<div class="health-bar">
|
||
<div
|
||
class="health-bar-fill"
|
||
:style="{ width: systemHealth.diskUsage + '%' }"
|
||
:class="getDiskUsageClass(systemHealth.diskUsage)"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
<div class="health-item">
|
||
<div class="health-label">Network I/O</div>
|
||
<div class="health-value">{{ formatBytes(systemHealth.networkRx) }} / {{ formatBytes(systemHealth.networkTx) }}</div>
|
||
</div>
|
||
<div class="health-item">
|
||
<div class="health-label">Active Connections</div>
|
||
<div class="health-value">{{ systemHealth.activeConnections }}</div>
|
||
</div>
|
||
<div class="health-item">
|
||
<div class="health-label">Error Rate</div>
|
||
<div class="health-value">{{ systemHealth.errorRate }}%</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Command History -->
|
||
<div class="command-history-section">
|
||
<h3>Command History</h3>
|
||
<div class="command-history">
|
||
<div
|
||
v-for="(command, index) in commandHistory.slice(0, 10)"
|
||
:key="index"
|
||
class="command-item"
|
||
>
|
||
<div class="command-header">
|
||
<div class="command-type">{{ command.command }}</div>
|
||
<div class="command-time">{{ formatTime(command.timestamp) }}</div>
|
||
<div class="command-status" :class="command.success ? 'success' : 'error'">
|
||
{{ command.success ? 'Success' : 'Error' }}
|
||
</div>
|
||
</div>
|
||
<div class="command-details">
|
||
<div v-if="command.params" class="command-params">
|
||
<strong>Parameters:</strong> {{ command.params }}
|
||
</div>
|
||
<div v-if="command.result" class="command-result">
|
||
<strong>Result:</strong>
|
||
<pre>{{ JSON.stringify(command.result, null, 2) }}</pre>
|
||
</div>
|
||
<div v-if="command.error" class="command-error">
|
||
<strong>Error:</strong> {{ command.error }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="commandHistory.length === 0" class="no-commands">
|
||
No commands executed yet
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Security Notice -->
|
||
<div class="security-notice">
|
||
<h4>⚠️ Security Notice</h4>
|
||
<p>Admin commands can have serious consequences. Please ensure you have proper authorization and understand the impact of each command before execution.</p>
|
||
<ul>
|
||
<li>Server restart will disconnect all clients</li>
|
||
<li>Database operations are irreversible</li>
|
||
<li>Always backup before destructive operations</li>
|
||
<li>Some commands require elevated permissions</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue'
|
||
import { useWebSocket } from '../../composables/useWebSocket'
|
||
|
||
const {
|
||
isConnected,
|
||
serverInfo,
|
||
systemHealth,
|
||
executeAdminCommand: executeAdminCommandFn,
|
||
getServerInfo,
|
||
getSystemHealth
|
||
} = useWebSocket()
|
||
|
||
const adminCommand = ref('restart_server')
|
||
const commandParams = ref('')
|
||
const customCommand = ref('')
|
||
const confirmationCode = ref('')
|
||
const commandHistory = ref<Array<{
|
||
command: string
|
||
params: string
|
||
result: any
|
||
error: string
|
||
success: boolean
|
||
timestamp: number
|
||
}>>([])
|
||
|
||
const executeAdminCommand = async () => {
|
||
if (!isConnected.value || !confirmationCode.value.trim()) return
|
||
|
||
let command = adminCommand.value
|
||
let params = {}
|
||
|
||
try {
|
||
if (adminCommand.value === 'custom') {
|
||
command = customCommand.value.trim()
|
||
if (!command) {
|
||
alert('Please enter a custom command')
|
||
return
|
||
}
|
||
}
|
||
|
||
if (commandParams.value.trim()) {
|
||
params = JSON.parse(commandParams.value)
|
||
}
|
||
|
||
const result = await executeAdminCommandFn(command, {
|
||
...params,
|
||
confirmationCode: confirmationCode.value.trim(),
|
||
timestamp: Date.now()
|
||
})
|
||
|
||
// Add to history
|
||
commandHistory.value.unshift({
|
||
command,
|
||
params: JSON.stringify(params),
|
||
result: result,
|
||
error: '',
|
||
success: true,
|
||
timestamp: Date.now()
|
||
})
|
||
|
||
// Clear form
|
||
confirmationCode.value = ''
|
||
commandParams.value = ''
|
||
|
||
} catch (error) {
|
||
// Add error to history
|
||
commandHistory.value.unshift({
|
||
command,
|
||
params: commandParams.value,
|
||
result: null,
|
||
error: error instanceof Error ? error.message : String(error),
|
||
success: false,
|
||
timestamp: Date.now()
|
||
})
|
||
}
|
||
}
|
||
|
||
const clearCommandHistory = () => {
|
||
commandHistory.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')}`
|
||
}
|
||
|
||
const formatBytes = (bytes: number) => {
|
||
if (bytes === 0) return '0 Bytes'
|
||
const k = 1024
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||
}
|
||
|
||
const getCpuUsageClass = (usage: number) => {
|
||
if (usage < 50) return 'usage-low'
|
||
if (usage < 80) return 'usage-medium'
|
||
return 'usage-high'
|
||
}
|
||
|
||
const getMemoryUsageClass = (usage: number) => {
|
||
if (usage < 60) return 'usage-low'
|
||
if (usage < 85) return 'usage-medium'
|
||
return 'usage-high'
|
||
}
|
||
|
||
const getDiskUsageClass = (usage: number) => {
|
||
if (usage < 70) return 'usage-low'
|
||
if (usage < 90) return 'usage-medium'
|
||
return 'usage-high'
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.admin-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);
|
||
}
|
||
|
||
.input-group input:disabled {
|
||
background: #e9ecef;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.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-danger {
|
||
background: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger:hover:not(:disabled) {
|
||
background: #c82333;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(220, 53, 69, 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-warning {
|
||
background: #FFC107;
|
||
color: #212529;
|
||
}
|
||
|
||
.btn-warning:hover {
|
||
background: #e0a800;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3);
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.server-info-section,
|
||
.system-health-section {
|
||
margin: 32px 0;
|
||
}
|
||
|
||
.server-info-section h3,
|
||
.system-health-section h3 {
|
||
margin-bottom: 16px;
|
||
color: #333;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.info-grid,
|
||
.health-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.info-item,
|
||
.health-item {
|
||
background: white;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
border: 1px solid #e9ecef;
|
||
text-align: center;
|
||
}
|
||
|
||
.info-label,
|
||
.health-label {
|
||
color: #666;
|
||
font-size: 14px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.info-value,
|
||
.health-value {
|
||
color: #333;
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.health-bar {
|
||
width: 100%;
|
||
height: 8px;
|
||
background: #e9ecef;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.health-bar-fill {
|
||
height: 100%;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.usage-low {
|
||
background: #28a745;
|
||
}
|
||
|
||
.usage-medium {
|
||
background: #ffc107;
|
||
}
|
||
|
||
.usage-high {
|
||
background: #dc3545;
|
||
}
|
||
|
||
.command-history-section {
|
||
margin-top: 32px;
|
||
}
|
||
|
||
.command-history-section h3 {
|
||
margin-bottom: 16px;
|
||
color: #333;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.command-history {
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
border: 1px solid #e9ecef;
|
||
}
|
||
|
||
.command-item {
|
||
margin: 16px 0;
|
||
background: white;
|
||
border-radius: 8px;
|
||
border: 1px solid #e9ecef;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.command-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
background: #f8f9fa;
|
||
border-bottom: 1px solid #e9ecef;
|
||
}
|
||
|
||
.command-type {
|
||
font-weight: 600;
|
||
color: #1976d2;
|
||
text-transform: uppercase;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.command-time {
|
||
color: #666;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.command-status {
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.command-status.success {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.command-status.error {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
}
|
||
|
||
.command-details {
|
||
padding: 16px;
|
||
}
|
||
|
||
.command-params,
|
||
.command-result,
|
||
.command-error {
|
||
margin: 12px 0;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.command-params strong,
|
||
.command-result strong,
|
||
.command-error strong {
|
||
color: #333;
|
||
display: block;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.command-result pre {
|
||
background: #f8f9fa;
|
||
padding: 12px;
|
||
border-radius: 4px;
|
||
overflow-x: auto;
|
||
font-size: 12px;
|
||
color: #333;
|
||
}
|
||
|
||
.command-error {
|
||
color: #dc3545;
|
||
background: #f8d7da;
|
||
padding: 12px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.no-commands {
|
||
text-align: center;
|
||
color: #666;
|
||
font-style: italic;
|
||
padding: 40px;
|
||
}
|
||
|
||
.security-notice {
|
||
margin-top: 32px;
|
||
padding: 20px;
|
||
background: #fff3cd;
|
||
border: 1px solid #ffeaa7;
|
||
border-radius: 8px;
|
||
border-left: 4px solid #ffc107;
|
||
}
|
||
|
||
.security-notice h4 {
|
||
margin: 0 0 12px 0;
|
||
color: #856404;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.security-notice p {
|
||
margin: 0 0 12px 0;
|
||
color: #856404;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.security-notice ul {
|
||
margin: 0;
|
||
padding-left: 20px;
|
||
color: #856404;
|
||
}
|
||
|
||
.security-notice li {
|
||
margin: 4px 0;
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* Responsive Design */
|
||
@media (max-width: 768px) {
|
||
.controls {
|
||
grid-template-columns: 1fr;
|
||
padding: 16px;
|
||
}
|
||
|
||
.info-grid,
|
||
.health-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|