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", 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: "/" },
|
||||||
|
|||||||
@@ -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 { 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user