6 Commits

Author SHA1 Message Date
jamie fc5699a04c Merge pull request 'feat: version number links to releases' (#54) from v2.0.1 into main
Reviewed-on: http://git.jdbnet.co.uk/jamie/ipam/pulls/54
2026-05-27 07:54:15 +01:00
jamie 675d477ff9 fix: 🐛 users page layout
Release / Build & Release (pull_request) Successful in 9s
Release / SonarQube (pull_request) Successful in 28s
2026-05-27 06:53:47 +00:00
jamie 34856060e8 refactor: 🎨 lock nav in place while content scrolls 2026-05-27 06:49:45 +00:00
jamie be55503e1c refactor: 🎨 remove status and alerting from dashboard 2026-05-27 06:48:26 +00:00
jamie b79763be53 fix: 🐛 searching for another device didn't work if already looking at a device 2026-05-27 06:47:14 +00:00
jamie e961afc36a feat: version number links to releases 2026-05-27 06:45:16 +00:00
5 changed files with 45 additions and 38 deletions
+16 -9
View File
@@ -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>
+3 -20
View File
@@ -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>
+10 -2
View File
@@ -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) {
+14 -3
View File
@@ -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">&lt;{{ u.email }}&gt;</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">&lt;{{ u.email }}&gt;</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>
+2 -4
View File
@@ -1,8 +1,6 @@
#!/bin/bash
set -e
if [ ! -f static/dist/index.html ]; then
echo "Building frontend..."
(cd frontend && npm ci && npm run build)
fi
echo "Building frontend..."
(cd frontend && npm ci && npm run build)
echo "Starting app..."
python app.py