frontend actualizado y mejorado extremadamente
This commit is contained in:
@@ -1,73 +1,124 @@
|
||||
<template>
|
||||
<main style="font-family: system-ui, sans-serif; padding: 16px; max-width: 980px; margin: 0 auto;">
|
||||
<h1>RADIUS Dashboard</h1>
|
||||
<section style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start;">
|
||||
<div>
|
||||
<h2>Usuarios</h2>
|
||||
<form @submit.prevent="createUser" style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px;">
|
||||
<input v-model="form.username" placeholder="usuario" required />
|
||||
<input v-model="form.password" placeholder="contraseña" required />
|
||||
<input v-model="form.vlan" placeholder="VLAN" />
|
||||
<label><input type="checkbox" v-model="form.disabled" /> deshabilitado</label>
|
||||
<button type="submit">Crear / Actualizar</button>
|
||||
<header class="topbar">
|
||||
<div class="title">RADIUS Nucleo</div>
|
||||
<div class="actions">
|
||||
<button class="icon-btn" @click="toggleTheme">
|
||||
<img class="icon" :src="theme === 'light' ? '/icons/moon.svg' : '/icons/sun.svg'" alt="theme">
|
||||
</button>
|
||||
<span class="chip"><span class="muted">Estado:</span> {{ statusText }}</span>
|
||||
<button class="icon-btn" @click="openAddUser">
|
||||
<img class="icon" src="/icons/user-plus.svg" alt="usuario"> Usuario
|
||||
</button>
|
||||
<button class="icon-btn" @click="openAddGuest">
|
||||
<img class="icon" src="/icons/guest.svg" alt="invitado"> Invitado
|
||||
</button>
|
||||
<button class="icon-btn" @click="openSettings">
|
||||
<img class="icon" src="/icons/settings.svg" alt="config"> Configuración
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="shell">
|
||||
<!-- Sidebar: Eventos -->
|
||||
<aside class="panel" :class="{ collapsed: sidebarCollapsed }">
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">Eventos FreeRADIUS</div>
|
||||
<div class="panel-actions">
|
||||
<button class="icon-btn" title="Filtrar" @click="showEventFilters = true"><img class="icon" src="/icons/filter.svg" alt="filtrar"></button>
|
||||
<button class="icon-btn" title="Limpiar" @click="clearRequests"><img class="icon" src="/icons/clear.svg" alt="limpiar"></button>
|
||||
<button class="icon-btn" title="Test" @click="selfTest"><img class="icon" src="/icons/test.svg" alt="test"></button>
|
||||
<a class="icon-btn" title="Descargar" href="/api/requests.csv" target="_blank"><img class="icon" src="/icons/download.svg" alt="descargar"></a>
|
||||
<button class="icon-btn" title="Copiar" @click="copyRequests"><img class="icon" src="/icons/copy.svg" alt="copiar"></button>
|
||||
<button class="icon-btn" title="Colapsar" @click="sidebarCollapsed = !sidebarCollapsed">{{ sidebarCollapsed ? 'Expandir' : 'Colapsar' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div v-if="loading.requests" class="muted">Cargando eventos…</div>
|
||||
<EventCard v-for="ev in filteredRequests" :key="ev.id" :ev="ev" />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main: Usuarios -->
|
||||
<main class="panel" :class="{ collapsed: mainCollapsed }">
|
||||
<div class="panel-header">
|
||||
<div class="row">
|
||||
<div class="panel-title">Usuarios y Dispositivos</div>
|
||||
<span class="spacer"></span>
|
||||
<button class="icon-btn" title="Vista usuarios" @click="layoutMode='user'">
|
||||
<img class="icon" src="/icons/layout-users.svg" alt="usuarios"/> Usuarios
|
||||
</button>
|
||||
<button class="icon-btn" title="Vista dispositivos" @click="layoutMode='device'">
|
||||
<img class="icon" src="/icons/layout-devices.svg" alt="dispositivos"/> Dispositivos
|
||||
</button>
|
||||
<button class="icon-btn" title="Filtrar" @click="showUserFilters = true"><img class="icon" src="/icons/filter.svg" alt="filtro"/></button>
|
||||
<button class="icon-btn" title="Colapsar" @click="mainCollapsed = !mainCollapsed">{{ mainCollapsed ? 'Expandir' : 'Colapsar' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<form @submit.prevent="createUser" class="row" style="margin-bottom:10px;">
|
||||
<input v-model="form.username" placeholder="usuario" required class="toggle"/>
|
||||
<input v-model="form.password" placeholder="contraseña" required class="toggle"/>
|
||||
<input v-model="form.vlan" placeholder="VLAN" class="toggle"/>
|
||||
<label class="row toggle" style="gap:6px;"><input type="checkbox" v-model="form.disabled"/> deshabilitado</label>
|
||||
<button type="submit" class="icon-btn">Crear / Actualizar</button>
|
||||
</form>
|
||||
<div v-if="loading.users">Cargando usuarios…</div>
|
||||
<table v-else style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left">Usuario</th>
|
||||
<th style="text-align:left">VLAN</th>
|
||||
<th style="text-align:left">Estado</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="u in users" :key="u.username">
|
||||
<td>{{ u.username }}</td>
|
||||
<td>{{ u.vlan }}</td>
|
||||
<td>{{ u.disabled ? 'deshabilitado' : 'activo' }}</td>
|
||||
<td>
|
||||
<button @click="toggleDisable(u)">{{ u.disabled ? 'Habilitar' : 'Deshabilitar' }}</button>
|
||||
<button @click="removeUser(u)" style="margin-left: 6px">Eliminar</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Eventos</h2>
|
||||
<div style="margin-bottom: 8px; display:flex; gap:8px;">
|
||||
<button @click="refreshRequests">Refrescar</button>
|
||||
<button @click="clearRequests">Limpiar</button>
|
||||
<button @click="selfTest">Self test</button>
|
||||
<a :href="'/api/requests.csv'" target="_blank">Descargar CSV</a>
|
||||
</div>
|
||||
<div v-if="loading.requests">Cargando eventos…</div>
|
||||
<div v-else style="max-height: 420px; overflow: auto; border: 1px solid #ddd; padding: 8px;">
|
||||
<div v-for="ev in requests" :key="ev.id" style="border-bottom: 1px dashed #ddd; padding: 6px 0;">
|
||||
<div><b>{{ ev.ts }}</b> — {{ ev.type }}</div>
|
||||
<div v-if="ev.attrs" style="font-size: 12px; color: #444;">
|
||||
<span>User: {{ ev.attrs['User-Name'] || ev.attrs['User-Name*0'] }}</span>
|
||||
<span v-if="ev.attrs['NAS-IP-Address']"> — NAS: {{ ev.attrs['NAS-IP-Address'] }}</span>
|
||||
<span v-if="ev.attrs['Calling-Station-Id']"> — STA: {{ ev.attrs['Calling-Station-Id'] }}</span>
|
||||
</div>
|
||||
<div v-if="ev.decision">Decision: {{ ev.decision }}</div>
|
||||
<div v-if="ev.error" style="color: #a00;">Error: {{ ev.error }}</div>
|
||||
</div>
|
||||
<div v-if="loading.users" class="muted">Cargando usuarios…</div>
|
||||
<div v-else class="grid">
|
||||
<UserCard v-for="u in filteredUsers" :key="u.username" :item="u" :mode="layoutMode"
|
||||
@toggleDisable="toggleDisable" @remove="removeUser" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</main>
|
||||
</section>
|
||||
|
||||
<!-- Modals -->
|
||||
<Modal :open="showEventFilters" title="Filtros de eventos" @close="showEventFilters=false">
|
||||
<div class="row" style="margin:8px 0;">
|
||||
<input v-model="eventFilters.text" placeholder="Buscar texto" class="toggle" style="flex:1;"/>
|
||||
<select v-model="eventFilters.type" class="toggle">
|
||||
<option value="">Todos</option>
|
||||
<option value="authorize">authorize</option>
|
||||
<option value="accounting">accounting</option>
|
||||
<option value="post-auth">post-auth</option>
|
||||
<option value="selftest">selftest</option>
|
||||
<option value="coa-disconnect">coa-disconnect</option>
|
||||
</select>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal :open="showUserFilters" title="Filtros de usuarios" @close="showUserFilters=false">
|
||||
<div class="row" style="margin:8px 0;">
|
||||
<input v-model="userFilters.text" placeholder="Buscar usuario" class="toggle" style="flex:1;"/>
|
||||
<select v-model="userFilters.status" class="toggle">
|
||||
<option value="">Todos</option>
|
||||
<option value="active">Activos</option>
|
||||
<option value="disabled">Deshabilitados</option>
|
||||
</select>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { onMounted, reactive, ref, computed } from 'vue';
|
||||
import EventCard from './components/EventCard.js';
|
||||
import UserCard from './components/UserCard.js';
|
||||
import Modal from './components/Modal.vue';
|
||||
|
||||
const users = ref([]);
|
||||
const requests = ref([]);
|
||||
const loading = reactive({ users: false, requests: false });
|
||||
const form = reactive({ username: '', password: '', vlan: '', disabled: false });
|
||||
|
||||
const showEventFilters = ref(false);
|
||||
const showUserFilters = ref(false);
|
||||
const eventFilters = reactive({ text: '', type: '' });
|
||||
const userFilters = reactive({ text: '', status: '' });
|
||||
const sidebarCollapsed = ref(false);
|
||||
const mainCollapsed = ref(false);
|
||||
const layoutMode = ref('user');
|
||||
const theme = ref(localStorage.getItem('theme') || 'dark');
|
||||
const statusText = ref('OK');
|
||||
|
||||
async function fetchUsers() {
|
||||
loading.users = true;
|
||||
try {
|
||||
@@ -138,12 +189,45 @@ onMounted(async () => {
|
||||
await fetchUsers();
|
||||
await fetchRequests();
|
||||
setupSse();
|
||||
applyTheme();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; }
|
||||
table th, table td { padding: 4px 6px; border-bottom: 1px solid #eee; }
|
||||
button { padding: 6px 10px; cursor: pointer; }
|
||||
input { padding: 6px 8px; }
|
||||
</style>
|
||||
const filteredRequests = computed(() => {
|
||||
return requests.value.filter(ev => {
|
||||
if (eventFilters.type && ev.type !== eventFilters.type) return false;
|
||||
if (eventFilters.text) {
|
||||
const t = eventFilters.text.toLowerCase();
|
||||
const blob = JSON.stringify(ev).toLowerCase();
|
||||
if (!blob.includes(t)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
const filteredUsers = computed(() => {
|
||||
return users.value.filter(u => {
|
||||
if (userFilters.text && !u.username.toLowerCase().includes(userFilters.text.toLowerCase())) return false;
|
||||
if (userFilters.status === 'active' && u.disabled) return false;
|
||||
if (userFilters.status === 'disabled' && !u.disabled) return false;
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
function copyRequests() {
|
||||
const txt = JSON.stringify(requests.value, null, 2);
|
||||
navigator.clipboard?.writeText(txt);
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
theme.value = theme.value === 'light' ? 'dark' : 'light';
|
||||
localStorage.setItem('theme', theme.value);
|
||||
applyTheme();
|
||||
}
|
||||
function applyTheme() {
|
||||
document.documentElement.classList.toggle('light', theme.value === 'light');
|
||||
}
|
||||
|
||||
function openAddUser() { /* placeholder for advanced modal */ }
|
||||
function openAddGuest() { /* placeholder for advanced modal */ }
|
||||
function openSettings() { /* placeholder for advanced modal */ }
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user