Compare commits
6 Commits
616744015f
..
v2.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| fc5699a04c | |||
| 675d477ff9 | |||
| 34856060e8 | |||
| be55503e1c | |||
| b79763be53 | |||
| e961afc36a |
@@ -5,6 +5,8 @@ import { Menu, Search, X, Home, Server, Grid3x3, Settings, Users, Tag, Layers, F
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { api } from "@/api";
|
||||
|
||||
const RELEASES_URL = "https://git.jdbnet.co.uk/jamie/ipam/releases";
|
||||
|
||||
const auth = useAuthStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -90,24 +92,29 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-screen bg-surface font-sans">
|
||||
<div class="flex h-screen overflow-hidden bg-surface font-sans">
|
||||
<!-- Mobile overlay -->
|
||||
<div v-if="sidebarOpen" class="fixed inset-0 z-40 bg-black/50 lg:hidden" @click="sidebarOpen = false" />
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
class="fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-r border-slate-200 bg-surface-raised transition-transform dark:border-slate-800 lg:static lg:translate-x-0"
|
||||
class="fixed inset-y-0 left-0 z-50 flex h-full w-64 shrink-0 flex-col border-r border-slate-200 bg-surface-raised transition-transform dark:border-slate-800 lg:static lg:h-screen lg:translate-x-0"
|
||||
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"
|
||||
>
|
||||
<div class="flex items-center gap-3 border-b border-slate-200 p-4 dark:border-slate-800">
|
||||
<div class="flex shrink-0 items-center gap-3 border-b border-slate-200 p-4 dark:border-slate-800">
|
||||
<img v-if="auth.org.logo" :src="auth.org.logo" alt="" class="h-8 rounded" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm font-semibold">{{ auth.org.name }} IPAM</div>
|
||||
<div class="text-xs text-slate-500">{{ auth.version }}</div>
|
||||
<a
|
||||
:href="RELEASES_URL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-xs text-slate-500 hover:text-accent hover:underline"
|
||||
>{{ auth.version }}</a>
|
||||
</div>
|
||||
<button class="lg:hidden" @click="sidebarOpen = false"><X class="h-5 w-5" /></button>
|
||||
</div>
|
||||
<nav class="flex-1 overflow-y-auto p-2">
|
||||
<nav class="min-h-0 flex-1 overflow-y-auto p-2">
|
||||
<RouterLink
|
||||
v-for="item in nav"
|
||||
:key="item.to"
|
||||
@@ -122,15 +129,15 @@ onUnmounted(() => {
|
||||
{{ item.label }}
|
||||
</RouterLink>
|
||||
</nav>
|
||||
<div class="border-t border-slate-200 p-3 dark:border-slate-800">
|
||||
<div class="shrink-0 border-t border-slate-200 p-3 dark:border-slate-800">
|
||||
<div class="truncate text-xs text-slate-500">{{ auth.user?.name }}</div>
|
||||
<button class="mt-2 text-xs text-accent hover:underline" @click="logout">Sign out</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main -->
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<header class="flex items-center gap-3 border-b border-slate-200 bg-surface-raised px-4 py-3 dark:border-slate-800">
|
||||
<div class="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<header class="flex shrink-0 items-center gap-3 border-b border-slate-200 bg-surface-raised px-4 py-3 dark:border-slate-800">
|
||||
<button class="lg:hidden" @click="sidebarOpen = true"><Menu class="h-6 w-6" /></button>
|
||||
<span class="font-semibold lg:hidden">{{ auth.org.name }} IPAM</span>
|
||||
<button
|
||||
@@ -141,7 +148,7 @@ onUnmounted(() => {
|
||||
<Search class="h-5 w-5" />
|
||||
</button>
|
||||
</header>
|
||||
<main class="flex-1 overflow-auto p-4 md:p-6">
|
||||
<main class="min-h-0 flex-1 overflow-auto p-4 md:p-6">
|
||||
<RouterView />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,6 @@ interface DashboardStats {
|
||||
available_ips: number;
|
||||
utilization_percent: number;
|
||||
subnet_count: number;
|
||||
alerting_subnets: number;
|
||||
device_count: number;
|
||||
}
|
||||
|
||||
@@ -22,7 +21,6 @@ interface SubnetOverviewRow {
|
||||
vlan_id?: number;
|
||||
utilization: number;
|
||||
available: number;
|
||||
status: "active" | "alerting";
|
||||
}
|
||||
|
||||
interface ActivityPoint {
|
||||
@@ -93,10 +91,7 @@ function formatHour(h: number) {
|
||||
<div>
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-slate-500">Subnets</div>
|
||||
<div class="mt-1 text-2xl font-bold">{{ stats.subnet_count }}</div>
|
||||
<div class="text-sm text-slate-500">
|
||||
Total
|
||||
<span v-if="stats.alerting_subnets" class="text-red-500">/ {{ stats.alerting_subnets }} alerting</span>
|
||||
</div>
|
||||
<div class="text-sm text-slate-500">Total</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card flex items-start gap-4">
|
||||
@@ -166,7 +161,6 @@ function formatHour(h: number) {
|
||||
<th class="p-2">Utilised</th>
|
||||
<th class="p-2">Available</th>
|
||||
<th class="p-2">Site</th>
|
||||
<th class="p-2">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -174,7 +168,6 @@ function formatHour(h: number) {
|
||||
v-for="s in subnetOverview"
|
||||
:key="s.id"
|
||||
class="border-b border-slate-100 dark:border-slate-800"
|
||||
:class="s.status === 'alerting' ? 'bg-red-500/5' : ''"
|
||||
>
|
||||
<td class="p-2">
|
||||
<RouterLink :to="`/subnets/${s.id}`" class="font-mono text-accent hover:underline">{{ s.cidr }}</RouterLink>
|
||||
@@ -184,8 +177,7 @@ function formatHour(h: number) {
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-1.5 w-16 overflow-hidden rounded-full bg-surface-overlay">
|
||||
<div
|
||||
class="h-full rounded-full"
|
||||
:class="s.status === 'alerting' ? 'bg-red-500' : 'bg-accent'"
|
||||
class="h-full rounded-full bg-accent"
|
||||
:style="{ width: `${s.utilization}%` }"
|
||||
/>
|
||||
</div>
|
||||
@@ -194,18 +186,9 @@ function formatHour(h: number) {
|
||||
</td>
|
||||
<td class="p-2">{{ s.available }}</td>
|
||||
<td class="p-2">{{ s.site }}</td>
|
||||
<td class="p-2">
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium"
|
||||
:class="s.status === 'alerting' ? 'bg-red-500/15 text-red-500' : 'bg-accent/15 text-accent'"
|
||||
>
|
||||
<span class="h-1.5 w-1.5 rounded-full" :class="s.status === 'alerting' ? 'bg-red-500' : 'bg-accent'" />
|
||||
{{ s.status === 'alerting' ? 'Alerting' : 'Active' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!subnetOverview.length">
|
||||
<td colspan="6" class="p-4 text-center text-slate-500">No subnets configured.</td>
|
||||
<td colspan="5" class="p-4 text-center text-slate-500">No subnets configured.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useRoute, useRouter, RouterLink } from "vue-router";
|
||||
import { api, type Device, type Tag, type Subnet } from "@/api";
|
||||
import type { IpHistoryEntry } from "@/components/IpHistoryModal.vue";
|
||||
@@ -70,7 +70,15 @@ async function loadDevice() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadDevice);
|
||||
watch(
|
||||
() => route.params.id,
|
||||
() => {
|
||||
showAssignIp.value = false;
|
||||
err.value = "";
|
||||
loadDevice();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function loadAvailableIps(subnetId: number) {
|
||||
if (!subnetId) {
|
||||
|
||||
@@ -140,10 +140,21 @@ async function delRole(id: number) {
|
||||
<button v-if="auth.can('manage_users')" class="btn-primary text-sm" @click="openAddUser">Add user</button>
|
||||
</div>
|
||||
<ul class="space-y-2">
|
||||
<li v-for="u in users" :key="u.id" class="card flex flex-wrap items-center justify-between gap-2">
|
||||
<span>{{ u.name }} <span class="text-slate-500"><{{ u.email }}></span></span>
|
||||
<li
|
||||
class="hidden px-4 text-xs font-medium text-slate-500 sm:grid sm:grid-cols-[minmax(0,1fr)_8rem_13rem] sm:items-center sm:gap-4"
|
||||
>
|
||||
<span>User</span>
|
||||
<span>Role</span>
|
||||
<span v-if="auth.can('manage_users')" class="text-right">Actions</span>
|
||||
</li>
|
||||
<li
|
||||
v-for="u in users"
|
||||
:key="u.id"
|
||||
class="card grid grid-cols-1 gap-2 sm:grid-cols-[minmax(0,1fr)_8rem_13rem] sm:items-center sm:gap-4"
|
||||
>
|
||||
<span class="min-w-0">{{ u.name }} <span class="text-slate-500"><{{ u.email }}></span></span>
|
||||
<span class="text-sm text-slate-500">{{ u.role_name }}</span>
|
||||
<div v-if="auth.can('manage_users')" class="flex gap-2">
|
||||
<div v-if="auth.can('manage_users')" class="flex gap-2 sm:justify-end">
|
||||
<button class="text-sm text-accent hover:underline" @click="openEditUser(u)">Edit</button>
|
||||
<button class="text-sm text-accent hover:underline" @click="regenKey(u.id)">API key</button>
|
||||
<button class="text-sm text-red-500 hover:underline" @click="delUser(u.id)">Delete</button>
|
||||
|
||||
Reference in New Issue
Block a user