From 974fe0b9e1a8b8b9072703e158a65f7099f67223 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Fri, 26 Sep 2025 16:54:39 -0600 Subject: [PATCH] frontend actualizado y mejorado extremadamente --- frontend/package-lock.json | 7 + frontend/package.json | 4 +- frontend/public/icons/clear.svg | 1 + frontend/public/icons/copy.svg | 1 + frontend/public/icons/download.svg | 1 + frontend/public/icons/filter.svg | 1 + frontend/public/icons/guest.svg | 1 + frontend/public/icons/layout-devices.svg | 1 + frontend/public/icons/layout-users.svg | 1 + frontend/public/icons/moon.svg | 1 + frontend/public/icons/settings.svg | 1 + frontend/public/icons/sun.svg | 1 + frontend/public/icons/test.svg | 1 + frontend/public/icons/user-plus.svg | 1 + frontend/src/App.vue | 212 ++++++++++++++++------- frontend/src/components/EventCard.js | 27 +++ frontend/src/components/Modal.vue | 24 +++ frontend/src/components/UserCard.js | 25 +++ frontend/src/main.js | 5 +- frontend/src/styles.css | 75 ++++++++ 20 files changed, 323 insertions(+), 68 deletions(-) create mode 100644 frontend/public/icons/clear.svg create mode 100644 frontend/public/icons/copy.svg create mode 100644 frontend/public/icons/download.svg create mode 100644 frontend/public/icons/filter.svg create mode 100644 frontend/public/icons/guest.svg create mode 100644 frontend/public/icons/layout-devices.svg create mode 100644 frontend/public/icons/layout-users.svg create mode 100644 frontend/public/icons/moon.svg create mode 100644 frontend/public/icons/settings.svg create mode 100644 frontend/public/icons/sun.svg create mode 100644 frontend/public/icons/test.svg create mode 100644 frontend/public/icons/user-plus.svg create mode 100644 frontend/src/components/EventCard.js create mode 100644 frontend/src/components/Modal.vue create mode 100644 frontend/src/components/UserCard.js create mode 100644 frontend/src/styles.css diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f740b50..cd1a4fe 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "radius-frontend", "version": "0.1.0", "dependencies": { + "htm": "^3.1.1", "vue": "^3.4.38" }, "devDependencies": { @@ -965,6 +966,12 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/htm": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", + "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", + "license": "Apache-2.0" + }, "node_modules/magic-string": { "version": "0.30.19", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", diff --git a/frontend/package.json b/frontend/package.json index 73ae48a..5837c3b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,11 +9,11 @@ "preview": "vite preview" }, "dependencies": { - "vue": "^3.4.38" + "vue": "^3.4.38", + "htm": "^3.1.1" }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.5", "vite": "^5.4.8" } } - diff --git a/frontend/public/icons/clear.svg b/frontend/public/icons/clear.svg new file mode 100644 index 0000000..46001e5 --- /dev/null +++ b/frontend/public/icons/clear.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/icons/copy.svg b/frontend/public/icons/copy.svg new file mode 100644 index 0000000..e2069e5 --- /dev/null +++ b/frontend/public/icons/copy.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/icons/download.svg b/frontend/public/icons/download.svg new file mode 100644 index 0000000..767e2c3 --- /dev/null +++ b/frontend/public/icons/download.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/icons/filter.svg b/frontend/public/icons/filter.svg new file mode 100644 index 0000000..7655ca5 --- /dev/null +++ b/frontend/public/icons/filter.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/icons/guest.svg b/frontend/public/icons/guest.svg new file mode 100644 index 0000000..99576d2 --- /dev/null +++ b/frontend/public/icons/guest.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/icons/layout-devices.svg b/frontend/public/icons/layout-devices.svg new file mode 100644 index 0000000..0ef6ece --- /dev/null +++ b/frontend/public/icons/layout-devices.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/icons/layout-users.svg b/frontend/public/icons/layout-users.svg new file mode 100644 index 0000000..8bfa2b9 --- /dev/null +++ b/frontend/public/icons/layout-users.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/icons/moon.svg b/frontend/public/icons/moon.svg new file mode 100644 index 0000000..d1cac4e --- /dev/null +++ b/frontend/public/icons/moon.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/icons/settings.svg b/frontend/public/icons/settings.svg new file mode 100644 index 0000000..3c9f566 --- /dev/null +++ b/frontend/public/icons/settings.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/icons/sun.svg b/frontend/public/icons/sun.svg new file mode 100644 index 0000000..2bbeeb8 --- /dev/null +++ b/frontend/public/icons/sun.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/icons/test.svg b/frontend/public/icons/test.svg new file mode 100644 index 0000000..13bfe95 --- /dev/null +++ b/frontend/public/icons/test.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/icons/user-plus.svg b/frontend/public/icons/user-plus.svg new file mode 100644 index 0000000..e3a457e --- /dev/null +++ b/frontend/public/icons/user-plus.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 84dcfcc..96df492 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,73 +1,124 @@ - +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 */ } + diff --git a/frontend/src/components/EventCard.js b/frontend/src/components/EventCard.js new file mode 100644 index 0000000..43c48d0 --- /dev/null +++ b/frontend/src/components/EventCard.js @@ -0,0 +1,27 @@ +import { defineComponent, h } from 'vue'; +import htm from 'htm'; +const html = htm.bind(h); + +export default defineComponent({ + name: 'EventCard', + props: { ev: { type: Object, required: true } }, + setup(props) { + return () => { + const a = props.ev.attrs || {}; + return html`
+
+ ${props.ev.type} + ${props.ev.ts} + ${props.ev.decision ? html`Decision: ${props.ev.decision}` : ''} + ${props.ev.error ? html`Error` : ''} +
+
+ ${a['User-Name'] || a['User-Name*0'] ? html`User: ${a['User-Name'] || a['User-Name*0']}` : ''} + ${a['NAS-IP-Address'] ? html` — NAS: ${a['NAS-IP-Address']}` : ''} + ${a['Calling-Station-Id'] ? html` — STA: ${a['Calling-Station-Id']}` : ''} +
+
`; + }; + } +}); + diff --git a/frontend/src/components/Modal.vue b/frontend/src/components/Modal.vue new file mode 100644 index 0000000..025172d --- /dev/null +++ b/frontend/src/components/Modal.vue @@ -0,0 +1,24 @@ + + + + diff --git a/frontend/src/components/UserCard.js b/frontend/src/components/UserCard.js new file mode 100644 index 0000000..62d21c5 --- /dev/null +++ b/frontend/src/components/UserCard.js @@ -0,0 +1,25 @@ +import { defineComponent, h } from 'vue'; +import htm from 'htm'; +const html = htm.bind(h); + +export default defineComponent({ + name: 'UserCard', + props: { item: { type: Object, required: true }, mode: { type: String, default: 'user' } }, + emits: ['toggleDisable', 'remove'], + setup(props, { emit }) { + function toggle() { emit('toggleDisable', props.item); } + function remove() { emit('remove', props.item); } + return () => html`
+
+ ${props.mode === 'user' ? props.item.username : (props.item.device || props.item.username)} + VLAN ${props.item.vlan} + ${props.item.disabled ? html`deshabilitado` : html`activo`} + + + +
+
${props.item.devices ? props.item.devices.length : 0} dispositivos
+
`; + } +}); + diff --git a/frontend/src/main.js b/frontend/src/main.js index 0e698cc..1570ac0 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,5 +1,6 @@ import { createApp } from 'vue'; import App from './App.vue'; +import './styles.css'; -createApp(App).mount('#app'); - +const app = createApp(App); +app.mount('#app'); diff --git a/frontend/src/styles.css b/frontend/src/styles.css new file mode 100644 index 0000000..4e3a22d --- /dev/null +++ b/frontend/src/styles.css @@ -0,0 +1,75 @@ +:root { + --bg: 15 15 18; + --fg: 235 235 240; + --muted: 180 180 190; + --accent: 80 160 255; + --card: 28 28 34 / 0.55; + --border: 255 255 255 / 0.12; + --glass-blur: 14px; + --radius: 14px; +} +:root.light { + --bg: 245 245 248; + --fg: 20 20 22; + --muted: 110 110 120; + --accent: 18 108 242; + --card: 255 255 255 / 0.6; + --border: 0 0 0 / 0.08; +} +* { box-sizing: border-box; } +html, body, #app { height: 100%; } +html, body { margin: 0; padding: 0; background: rgb(var(--bg)); color: rgb(var(--fg)); } +body { font: 14px/1.45 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; } +button { cursor: pointer; } +a { color: inherit; } + +/* Top bar */ +.topbar { + position: sticky; top: 0; z-index: 10; + display: flex; flex-wrap: wrap; align-items: center; + gap: 10px; padding: 10px 14px; backdrop-filter: blur(var(--glass-blur)); + background: linear-gradient(180deg, rgba(var(--card)), rgba(var(--card)) 60%, rgba(0,0,0,0)); + border-bottom: 1px solid rgba(var(--border)); +} +.title { font-size: 16px; font-weight: 700; letter-spacing: .2px; flex: 1 1 auto; } +.actions { display: inline-flex; flex-wrap: wrap; gap: 8px; align-items: center; } +.icon-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 10px; border-radius: 10px; border: 1px solid rgba(var(--border)); background: rgba(var(--card)); color: inherit; transition: transform .12s ease, background .2s; + backdrop-filter: blur(var(--glass-blur)); } +.icon-btn:hover { transform: translateY(-1px); background: rgba(var(--card)); } +.icon { width: 16px; height: 16px; opacity: .9; } + +/* Layout */ +.shell { height: calc(100vh - 54px); display: grid; grid-template-columns: 360px 1fr; gap: 12px; padding: 12px; } +.panel { border: 1px solid rgba(var(--border)); background: rgba(var(--card)); border-radius: var(--radius); backdrop-filter: blur(var(--glass-blur)); overflow: hidden; display: flex; flex-direction: column; min-height: 0; } +.panel-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid rgba(var(--border)); } +.panel-title { font-weight: 600; } +.panel-actions { display: inline-flex; flex-wrap: wrap; gap: 6px; } +.scroll { overflow: auto; padding: 10px; } + +/* Cards */ +.card { border: 1px solid rgba(var(--border)); background: rgba(var(--card)); border-radius: 12px; padding: 10px; transition: transform .12s ease, box-shadow .2s ease; box-shadow: 0 4px 14px rgba(0,0,0,.08); } +.card:hover { transform: translateY(-1px); box-shadow: 0 8px 20px rgba(0,0,0,.12); } +.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 10px; } + +/* Responsive */ +@media (max-width: 980px) { + .shell { grid-template-columns: 1fr; } +} + +/* Collapse helpers: keep headers visible when collapsed */ +.panel.collapsed .scroll { display: none; } + +/* Modal */ +.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.35); backdrop-filter: blur(4px); display: grid; place-items: center; z-index: 20; animation: fadeIn .15s ease; +} +.modal { width: min(680px, 92vw); border-radius: 14px; border: 1px solid rgba(var(--border)); background: rgba(var(--card)); padding: 14px; box-shadow: 0 10px 32px rgba(0,0,0,.2); } +.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } +.modal-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; } +@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } + +/* Small bits */ +.chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 8px; border: 1px solid rgba(var(--border)); border-radius: 999px; background: rgba(var(--card)); font-size: 12px; } +.muted { color: rgb(var(--muted)); } +.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } +.spacer { flex: 1; } +.toggle { padding: 6px 10px; border-radius: 8px; border: 1px solid rgba(var(--border)); background: rgba(var(--card)); }