refactor: 🎨 remove caching #48
@@ -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>
|
||||
@@ -15,7 +15,7 @@ const router = createRouter({
|
||||
{ path: "devices", name: "devices", component: () => import("@/views/DevicesView.vue") },
|
||||
{ path: "devices/:id", name: "device", component: () => import("@/views/DeviceDetailView.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/:id", name: "rack", component: () => import("@/views/RackDetailView.vue") },
|
||||
{ path: "search", redirect: "/" },
|
||||
|
||||
@@ -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>
|
||||
@@ -4,15 +4,19 @@ import { useRoute, RouterLink } from "vue-router";
|
||||
import { api, type Subnet } from "@/api";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import IpHistoryModal from "@/components/IpHistoryModal.vue";
|
||||
import DhcpModal from "@/components/DhcpModal.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const auth = useAuthStore();
|
||||
const subnet = ref<Subnet | 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));
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(loadSubnet);
|
||||
|
||||
async function saveNotes(ipId: number, notes: string) {
|
||||
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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,5 +76,6 @@ async function saveNotes(ipId: number, notes: string) {
|
||||
</table>
|
||||
</div>
|
||||
<IpHistoryModal :ip="historyIp" @close="historyIp = null" />
|
||||
<DhcpModal :open="showDhcp" :subnet-id="subnet.id" @close="showDhcp = false" @saved="loadSubnet" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user