Initial commit: AMS Frontend - Vue 3 + PrimeVue
This commit is contained in:
296
src/layouts/AppLayout.vue
Normal file
296
src/layouts/AppLayout.vue
Normal file
@@ -0,0 +1,296 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useWebSocketStore } from '../stores/websocket'
|
||||
import Popover from 'primevue/popover'
|
||||
import WsStatsPanel from '../components/WsStatsPanel.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const wsStore = useWebSocketStore()
|
||||
const wsPopover = ref()
|
||||
const sidebarOpen = ref(false)
|
||||
|
||||
// Close sidebar on route change (mobile)
|
||||
watch(() => route.path, () => {
|
||||
sidebarOpen.value = false
|
||||
})
|
||||
|
||||
const feVersion = import.meta.env.VITE_APP_VERSION || 'dev'
|
||||
const beVersion = ref('...')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const apiBase = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'
|
||||
const res = await fetch(`${apiBase}/version`)
|
||||
const data = await res.json()
|
||||
beVersion.value = data.version || 'unknown'
|
||||
} catch {
|
||||
beVersion.value = '?'
|
||||
}
|
||||
|
||||
wsStore.connect()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
wsStore.disconnect()
|
||||
})
|
||||
|
||||
function handleLogout() {
|
||||
wsStore.disconnect()
|
||||
authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
function toggleWsStats(event: Event) {
|
||||
wsPopover.value?.toggle(event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-layout">
|
||||
<!-- Mobile Hamburger -->
|
||||
<button class="hamburger" @click="sidebarOpen = !sidebarOpen" :class="{ open: sidebarOpen }">
|
||||
<i :class="sidebarOpen ? 'pi pi-times' : 'pi pi-bars'"></i>
|
||||
</button>
|
||||
|
||||
<!-- Overlay -->
|
||||
<div v-if="sidebarOpen" class="sidebar-overlay" @click="sidebarOpen = false"></div>
|
||||
|
||||
<aside class="sidebar" :class="{ open: sidebarOpen }">
|
||||
<div class="sidebar-header">
|
||||
<h2>🤖 AMS</h2>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav-container">
|
||||
<ul class="sidebar-nav">
|
||||
<li>
|
||||
<router-link to="/">
|
||||
<i class="pi pi-home"></i>
|
||||
Dashboard
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/agents">
|
||||
<i class="pi pi-users"></i>
|
||||
Agents
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/projects">
|
||||
<i class="pi pi-folder"></i>
|
||||
Projekte
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/tasks">
|
||||
<i class="pi pi-list-check"></i>
|
||||
Tasks
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/logs">
|
||||
<i class="pi pi-file"></i>
|
||||
Logs
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/labels">
|
||||
<i class="pi pi-tags"></i>
|
||||
Labels
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/gitlab">
|
||||
<i class="pi pi-github"></i>
|
||||
GitLab
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/agent-task">
|
||||
<i class="pi pi-send"></i>
|
||||
Aufgabe stellen
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/agent-tasks-overview">
|
||||
<i class="pi pi-list-check"></i>
|
||||
Agent-Aufträge
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/cronjobs">
|
||||
<i class="pi pi-clock"></i>
|
||||
CronJobs
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/workspace">
|
||||
<i class="pi pi-folder-open"></i>
|
||||
Agent-Dateien
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/secrets">
|
||||
<i class="pi pi-lock"></i>
|
||||
Secrets
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/token-analytics">
|
||||
<i class="pi pi-chart-line"></i>
|
||||
Token Analytics
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="authStore.user?.role === 'admin'">
|
||||
<router-link to="/containers">
|
||||
<i class="pi pi-box"></i>
|
||||
Container
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/settings">
|
||||
<i class="pi pi-cog"></i>
|
||||
Einstellungen
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div style="color: rgba(255,255,255,0.5); font-size: 0.875rem; margin-bottom: 0.5rem;">
|
||||
{{ authStore.user?.username }}
|
||||
</div>
|
||||
<button
|
||||
@click="handleLogout"
|
||||
style="background: none; border: 1px solid rgba(255,255,255,0.2); color: rgba(255,255,255,0.7); padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; width: 100%;"
|
||||
>
|
||||
<i class="pi pi-sign-out" style="margin-right: 0.5rem;"></i>
|
||||
Abmelden
|
||||
</button>
|
||||
<div class="sidebar-version">
|
||||
<span class="ws-indicator" :class="{ online: wsStore.isConnected }" :title="wsStore.isConnected ? 'WebSocket verbunden' : 'WebSocket getrennt'" @click="toggleWsStats" style="cursor: pointer;">●</span>
|
||||
FE {{ feVersion }} | BE {{ beVersion }}
|
||||
</div>
|
||||
<Popover ref="wsPopover">
|
||||
<WsStatsPanel />
|
||||
</Popover>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hamburger {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0.75rem;
|
||||
left: 0.75rem;
|
||||
z-index: 1100;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
background: rgba(26, 26, 46, 0.95);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(8px);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.hamburger:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sidebar-overlay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hamburger {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sidebar-overlay {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
transform: translateX(-100%);
|
||||
width: 220px;
|
||||
background: #0f0f1e;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0 !important;
|
||||
padding-top: 3.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-version {
|
||||
text-align: center;
|
||||
margin-top: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.ws-indicator {
|
||||
color: rgba(239, 68, 68, 0.6);
|
||||
font-size: 0.5rem;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
|
||||
.ws-indicator.online {
|
||||
color: rgba(34, 197, 94, 0.8);
|
||||
}
|
||||
|
||||
/* Popover Dark Theme */
|
||||
:deep(.p-popover) {
|
||||
background: #1a1a2e !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
:deep(.p-popover-content) {
|
||||
background: #1a1a2e !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
:deep(.p-popover)::before,
|
||||
:deep(.p-popover)::after {
|
||||
border-color: transparent !important;
|
||||
border-bottom-color: #1a1a2e !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user