refactor: 🎨 remove caching #48

Merged
jamie merged 15 commits from v2.0.0 into main 2026-05-23 21:04:45 +01:00
4 changed files with 168 additions and 55 deletions
Showing only changes of commit 39a8f4a49b - Show all commits
+152
View File
@@ -0,0 +1,152 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { X } from "lucide-vue-next";
import { api } from "@/api";
import { useAuthStore } from "@/stores/auth";
const props = defineProps<{
open: boolean;
subnetId: number | null;
}>();
const emit = defineEmits<{ close: []; saved: [] }>();
const auth = useAuthStore();
const loading = ref(false);
const saving = ref(false);
const err = ref("");
const msg = ref("");
const hasPool = ref(false);
const form = ref({ start_ip: "", end_ip: "", excluded_ips: "" });
const canEdit = () => auth.can("configure_dhcp");
async function loadPool() {
if (!props.subnetId) return;
loading.value = true;
err.value = "";
msg.value = "";
hasPool.value = false;
form.value = { start_ip: "", end_ip: "", excluded_ips: "" };
try {
const d = await api.getDhcp(props.subnetId) as { pools?: { start_ip: string; end_ip: string; excluded_ips?: string }[] };
if (d.pools?.[0]) {
hasPool.value = true;
form.value.start_ip = d.pools[0].start_ip;
form.value.end_ip = d.pools[0].end_ip;
form.value.excluded_ips = d.pools[0].excluded_ips || "";
}
} catch (e) {
if (auth.can("view_dhcp")) {
err.value = e instanceof Error ? e.message : "Failed to load DHCP pool";
}
} finally {
loading.value = false;
}
}
watch(
() => [props.open, props.subnetId] as const,
([open]) => {
if (open) loadPool();
},
);
async function save() {
if (!props.subnetId || !canEdit()) return;
saving.value = true;
err.value = "";
msg.value = "";
try {
await api.setDhcp(props.subnetId, {
pools: [{
start_ip: form.value.start_ip,
end_ip: form.value.end_ip,
excluded_ips: form.value.excluded_ips.split(",").map((s) => s.trim()).filter(Boolean),
}],
});
hasPool.value = true;
msg.value = "Saved";
emit("saved");
} catch (e) {
err.value = e instanceof Error ? e.message : "Failed to save";
} finally {
saving.value = false;
}
}
async function remove() {
if (!props.subnetId || !canEdit() || !confirm("Remove this DHCP pool?")) return;
saving.value = true;
err.value = "";
msg.value = "";
try {
await api.setDhcp(props.subnetId, { remove: true });
hasPool.value = false;
form.value = { start_ip: "", end_ip: "", excluded_ips: "" };
msg.value = "Removed";
emit("saved");
} catch (e) {
err.value = e instanceof Error ? e.message : "Failed to remove";
} finally {
saving.value = false;
}
}
function onKeydown(e: KeyboardEvent) {
if (e.key === "Escape") emit("close");
}
</script>
<template>
<Teleport to="body">
<div
v-if="open"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
@click.self="emit('close')"
@keydown="onKeydown"
>
<form class="card w-full max-w-lg space-y-4 shadow-xl" @submit.prevent="save">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold">DHCP pool</h2>
<button type="button" class="rounded-lg p-1 hover:bg-surface-overlay" aria-label="Close" @click="emit('close')">
<X class="h-5 w-5" />
</button>
</div>
<p v-if="loading" class="text-sm text-slate-500">Loading</p>
<template v-else>
<input
v-model="form.start_ip"
class="input-field"
placeholder="Start IP"
required
:disabled="!canEdit()"
/>
<input
v-model="form.end_ip"
class="input-field"
placeholder="End IP"
required
:disabled="!canEdit()"
/>
<input
v-model="form.excluded_ips"
class="input-field"
placeholder="Excluded IPs (comma-separated)"
:disabled="!canEdit()"
/>
<div v-if="canEdit()" class="flex gap-2">
<button type="submit" class="btn-primary" :disabled="saving">Save</button>
<button v-if="hasPool" type="button" class="btn-secondary" :disabled="saving" @click="remove">Remove pool</button>
<button type="button" class="btn-secondary" @click="emit('close')">Cancel</button>
</div>
<div v-else class="flex justify-end">
<button type="button" class="btn-secondary" @click="emit('close')">Close</button>
</div>
<p v-if="msg" class="text-sm text-accent">{{ msg }}</p>
<p v-if="err" class="text-sm text-red-500">{{ err }}</p>
</template>
</form>
</div>
</Teleport>
</template>
+1 -1
View File
@@ -15,7 +15,7 @@ const router = createRouter({
{ path: "devices", name: "devices", component: () => import("@/views/DevicesView.vue") }, { path: "devices", name: "devices", component: () => import("@/views/DevicesView.vue") },
{ path: "devices/:id", name: "device", component: () => import("@/views/DeviceDetailView.vue") }, { path: "devices/:id", name: "device", component: () => import("@/views/DeviceDetailView.vue") },
{ path: "subnets/:id", name: "subnet", component: () => import("@/views/SubnetDetailView.vue") }, { path: "subnets/:id", name: "subnet", component: () => import("@/views/SubnetDetailView.vue") },
{ path: "subnets/:id/dhcp", name: "dhcp", component: () => import("@/views/DhcpView.vue") }, { path: "subnets/:id/dhcp", redirect: (to) => `/subnets/${to.params.id}` },
{ path: "racks", name: "racks", component: () => import("@/views/RacksView.vue") }, { path: "racks", name: "racks", component: () => import("@/views/RacksView.vue") },
{ path: "racks/:id", name: "rack", component: () => import("@/views/RackDetailView.vue") }, { path: "racks/:id", name: "rack", component: () => import("@/views/RackDetailView.vue") },
{ path: "search", redirect: "/" }, { path: "search", redirect: "/" },
-51
View File
@@ -1,51 +0,0 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useRoute, RouterLink } from "vue-router";
import { api } from "@/api";
const route = useRoute();
const pool = ref<{ start_ip?: string; end_ip?: string; excluded_ips?: string } | null>(null);
const form = ref({ start_ip: "", end_ip: "", excluded_ips: "" });
const msg = ref("");
onMounted(async () => {
try {
const d = await api.getDhcp(Number(route.params.id)) as { pools?: { start_ip: string; end_ip: string; excluded_ips?: string }[] };
if (d.pools?.[0]) {
pool.value = d.pools[0];
form.value.start_ip = d.pools[0].start_ip;
form.value.end_ip = d.pools[0].end_ip;
form.value.excluded_ips = d.pools[0].excluded_ips || "";
}
} catch { /* no pool */ }
});
async function save() {
await api.setDhcp(Number(route.params.id), {
pools: [{ start_ip: form.value.start_ip, end_ip: form.value.end_ip, excluded_ips: form.value.excluded_ips.split(",").map((s) => s.trim()).filter(Boolean) }],
});
msg.value = "Saved";
}
async function remove() {
await api.setDhcp(Number(route.params.id), { remove: true });
pool.value = null;
msg.value = "Removed";
}
</script>
<template>
<div>
<RouterLink :to="`/subnets/${route.params.id}`" class="text-sm text-accent hover:underline"> Subnet</RouterLink>
<h1 class="mt-4 text-2xl font-bold">DHCP pool</h1>
<form class="card mt-6 max-w-lg space-y-4" @submit.prevent="save">
<input v-model="form.start_ip" class="input-field" placeholder="Start IP" required />
<input v-model="form.end_ip" class="input-field" placeholder="End IP" required />
<input v-model="form.excluded_ips" class="input-field" placeholder="Excluded IPs (comma-separated)" />
<div class="flex gap-2">
<button type="submit" class="btn-primary">Save</button>
<button v-if="pool" type="button" class="btn-secondary" @click="remove">Remove pool</button>
</div>
<p v-if="msg" class="text-sm text-accent">{{ msg }}</p>
</form>
</div>
</template>
+15 -3
View File
@@ -4,15 +4,19 @@ import { useRoute, RouterLink } from "vue-router";
import { api, type Subnet } from "@/api"; import { api, type Subnet } from "@/api";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import IpHistoryModal from "@/components/IpHistoryModal.vue"; import IpHistoryModal from "@/components/IpHistoryModal.vue";
import DhcpModal from "@/components/DhcpModal.vue";
const route = useRoute(); const route = useRoute();
const auth = useAuthStore(); const auth = useAuthStore();
const subnet = ref<Subnet | null>(null); const subnet = ref<Subnet | null>(null);
const historyIp = ref<string | null>(null); const historyIp = ref<string | null>(null);
const showDhcp = ref(false);
onMounted(async () => { async function loadSubnet() {
subnet.value = await api.subnet(Number(route.params.id)); subnet.value = await api.subnet(Number(route.params.id));
}); }
onMounted(loadSubnet);
async function saveNotes(ipId: number, notes: string) { async function saveNotes(ipId: number, notes: string) {
await api.patchIpNotes(ipId, notes); await api.patchIpNotes(ipId, notes);
@@ -27,7 +31,14 @@ async function saveNotes(ipId: number, notes: string) {
<p class="font-mono text-slate-500">{{ subnet.cidr }} · {{ subnet.site || "Unassigned" }}</p> <p class="font-mono text-slate-500">{{ subnet.cidr }} · {{ subnet.site || "Unassigned" }}</p>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<RouterLink :to="`/subnets/${subnet.id}/dhcp`" class="btn-secondary text-sm">DHCP</RouterLink> <button
v-if="auth.can('view_dhcp') || auth.can('configure_dhcp')"
type="button"
class="btn-secondary text-sm"
@click="showDhcp = true"
>
DHCP
</button>
<a v-if="auth.can('export_subnet_csv')" :href="`/api/v2/subnets/${subnet.id}/export`" class="btn-secondary text-sm">Export CSV</a> <a v-if="auth.can('export_subnet_csv')" :href="`/api/v2/subnets/${subnet.id}/export`" class="btn-secondary text-sm">Export CSV</a>
</div> </div>
</div> </div>
@@ -65,5 +76,6 @@ async function saveNotes(ipId: number, notes: string) {
</table> </table>
</div> </div>
<IpHistoryModal :ip="historyIp" @close="historyIp = null" /> <IpHistoryModal :ip="historyIp" @close="historyIp = null" />
<DhcpModal :open="showDhcp" :subnet-id="subnet.id" @close="showDhcp = false" @saved="loadSubnet" />
</div> </div>
</template> </template>