refactor importing interfaces to its own modal component

This commit is contained in:
liamcottle
2025-01-01 16:07:04 +13:00
parent 80db27da07
commit b12aa387bd
2 changed files with 158 additions and 127 deletions

View File

@@ -0,0 +1,149 @@
<template>
<div v-if="showingImportDialog" class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity flex items-center justify-center">
<div class="bg-white dark:bg-zinc-900 rounded-lg shadow-xl max-w-2xl w-full mx-4">
<div class="p-4 border-b dark:border-zinc-700">
<h3 class="text-lg font-semibold dark:text-white">Import Interfaces</h3>
</div>
<div class="p-4 space-y-4">
<!-- File Input -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-zinc-200">Select Configuration File</label>
<input type="file"
@change="onFileSelected"
accept="*"
class="mt-1 block w-full text-sm text-gray-500 dark:text-zinc-400
file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0
file:text-sm file:font-semibold
file:bg-gray-500 file:text-white
hover:file:bg-gray-400
dark:file:bg-zinc-700 dark:hover:file:bg-zinc-600">
</div>
<!-- Interface Selection -->
<div v-if="importableInterfaces.length > 0">
<div class="flex justify-between mb-2">
<label class="block text-sm font-medium text-gray-700 dark:text-zinc-200">Select Interfaces to Import</label>
<div class="space-x-2">
<button @click="selectAllInterfaces" class="text-sm text-blue-500">Select All</button>
<button @click="deselectAllInterfaces" class="text-sm text-blue-500">Deselect All</button>
</div>
</div>
<div class="space-y-2 max-h-60 overflow-y-auto">
<div v-for="iface in importableInterfaces" :key="iface.name"
class="flex items-center p-2 border rounded dark:border-zinc-700">
<input type="checkbox"
v-model="selectedInterfaces"
:value="iface.name"
class="h-4 w-4 text-blue-600 rounded border-gray-300 dark:border-zinc-600">
<label class="ml-2 text-sm text-gray-700 dark:text-zinc-200">
{{ iface.name }} ({{ iface.type }})
</label>
</div>
</div>
</div>
</div>
<div class="p-4 border-t dark:border-zinc-700 flex justify-end space-x-2">
<button @click="dismiss" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 dark:bg-zinc-800 dark:text-zinc-200 dark:border-zinc-600 dark:hover:bg-zinc-700">
Cancel
</button>
<button @click="importSelectedInterfaces" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600">
Import Selected
</button>
</div>
</div>
</div>
</template>
<script>
import DialogUtils from "../../js/DialogUtils";
export default {
name: "ImportInterfacesModal",
emits: [
"dismissed",
],
data() {
return {
showingImportDialog: false,
importableInterfaces: [],
selectedInterfaces: [],
importFile: null,
};
},
methods: {
show() {
this.showingImportDialog = true;
this.importableInterfaces = [];
this.selectedInterfaces = [];
this.importFile = null;
},
dismiss() {
this.showingImportDialog = false;
this.$emit("dismissed");
},
async onFileSelected(event) {
const file = event.target.files[0];
if (!file) return;
this.importFile = file;
this.importableInterfaces = [];
this.selectedInterfaces = [];
const formData = new FormData();
formData.append('config', file);
try {
const response = await window.axios.post('/api/v1/reticulum/interfaces/preview', formData);
if (response.data.interfaces && response.data.interfaces.length > 0) {
this.importableInterfaces = response.data.interfaces;
this.selectedInterfaces = this.importableInterfaces.map(i => i.name);
} else {
DialogUtils.alert("No valid interfaces found in configuration file");
this.dismiss();
}
} catch(e) {
DialogUtils.alert("Failed to parse configuration file");
console.error(e);
this.dismiss();
}
},
selectAllInterfaces() {
this.selectedInterfaces = this.importableInterfaces.map(i => i.name);
},
deselectAllInterfaces() {
this.selectedInterfaces = [];
},
async importSelectedInterfaces() {
if (!this.importFile) {
DialogUtils.alert("Please select a configuration file");
return;
}
if (this.selectedInterfaces.length === 0) {
DialogUtils.alert("Please select at least one interface to import");
return;
}
const formData = new FormData();
formData.append('config', this.importFile);
formData.append('selected_interfaces', JSON.stringify(this.selectedInterfaces));
try {
await window.axios.post('/api/v1/reticulum/interfaces/import', formData);
this.dismiss();
DialogUtils.alert("Interfaces imported successfully");
} catch(e) {
const message = e.response?.data?.message || "Failed to import interfaces";
DialogUtils.alert(message);
console.error(e);
}
},
},
}
</script>

View File

@@ -32,7 +32,7 @@
<!-- Import button -->
<div class="my-auto">
<button @click="showImportDialog" type="button" class="inline-flex items-center gap-x-1 rounded-md bg-gray-500 dark:bg-zinc-700 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 dark:hover:bg-zinc-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:focus-visible:outline-zinc-700">
<button @click="showImportInterfacesModal" type="button" class="inline-flex items-center gap-x-1 rounded-md bg-gray-500 dark:bg-zinc-700 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 dark:hover:bg-zinc-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:focus-visible:outline-zinc-700">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
@@ -74,64 +74,8 @@
</div>
<!-- Import Dialog -->
<div v-if="showingImportDialog" class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity flex items-center justify-center">
<div class="bg-white dark:bg-zinc-900 rounded-lg shadow-xl max-w-2xl w-full mx-4">
<div class="p-4 border-b dark:border-zinc-700">
<h3 class="text-lg font-semibold dark:text-white">Import Interfaces</h3>
</div>
<div class="p-4 space-y-4">
<!-- File Input -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-zinc-200">Select Configuration File</label>
<input type="file"
@change="onFileSelected"
accept="*"
class="mt-1 block w-full text-sm text-gray-500 dark:text-zinc-400
file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0
file:text-sm file:font-semibold
file:bg-gray-500 file:text-white
hover:file:bg-gray-400
dark:file:bg-zinc-700 dark:hover:file:bg-zinc-600">
</div>
<ImportInterfacesModal ref="import-interfaces-modal" @dismissed="onImportInterfacesModalDismissed"/>
<!-- Interface Selection -->
<div v-if="importableInterfaces.length > 0">
<div class="flex justify-between mb-2">
<label class="block text-sm font-medium text-gray-700 dark:text-zinc-200">Select Interfaces to Import</label>
<div class="space-x-2">
<button @click="selectAllInterfaces" class="text-sm text-blue-500">Select All</button>
<button @click="deselectAllInterfaces" class="text-sm text-blue-500">Deselect All</button>
</div>
</div>
<div class="space-y-2 max-h-60 overflow-y-auto">
<div v-for="iface in importableInterfaces" :key="iface.name"
class="flex items-center p-2 border rounded dark:border-zinc-700">
<input type="checkbox"
v-model="selectedInterfaces"
:value="iface.name"
class="h-4 w-4 text-blue-600 rounded border-gray-300 dark:border-zinc-600">
<label class="ml-2 text-sm text-gray-700 dark:text-zinc-200">
{{ iface.name }} ({{ iface.type }})
</label>
</div>
</div>
</div>
</div>
<div class="p-4 border-t dark:border-zinc-700 flex justify-end space-x-2">
<button @click="closeImportDialog"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 dark:bg-zinc-800 dark:text-zinc-200 dark:border-zinc-600 dark:hover:bg-zinc-700">
Cancel
</button>
<button @click="importSelectedInterfaces"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600">
Import Selected
</button>
</div>
</div>
</div>
</template>
<script>
@@ -139,10 +83,12 @@ import DialogUtils from "../../js/DialogUtils";
import ElectronUtils from "../../js/ElectronUtils";
import Interface from "./Interface.vue";
import Utils from "../../js/Utils";
import ImportInterfacesModal from "./ImportInterfacesModal.vue";
export default {
name: 'InterfacesPage',
components: {
ImportInterfacesModal,
Interface,
},
data() {
@@ -150,10 +96,6 @@ export default {
interfaces: {},
interfaceStats: {},
reloadInterval: null,
showingImportDialog: false,
importableInterfaces: [],
selectedInterfaces: [],
importFile: null
};
},
beforeUnmount() {
@@ -324,73 +266,13 @@ export default {
console.error(e);
}
},
showImportDialog() {
this.showingImportDialog = true;
this.importableInterfaces = [];
this.selectedInterfaces = [];
this.importFile = null;
showImportInterfacesModal() {
this.$refs["import-interfaces-modal"].show();
},
closeImportDialog() {
this.showingImportDialog = false;
onImportInterfacesModalDismissed() {
// reload interfaces as something may have been imported
this.loadInterfaces();
},
async onFileSelected(event) {
const file = event.target.files[0];
if (!file) return;
this.importFile = file;
this.importableInterfaces = [];
this.selectedInterfaces = [];
const formData = new FormData();
formData.append('config', file);
try {
const response = await window.axios.post('/api/v1/reticulum/interfaces/preview', formData);
if (response.data.interfaces && response.data.interfaces.length > 0) {
this.importableInterfaces = response.data.interfaces;
this.selectedInterfaces = this.importableInterfaces.map(i => i.name);
} else {
DialogUtils.alert("No valid interfaces found in configuration file");
this.closeImportDialog();
}
} catch(e) {
DialogUtils.alert("Failed to parse configuration file");
console.error(e);
this.closeImportDialog();
}
},
selectAllInterfaces() {
this.selectedInterfaces = this.importableInterfaces.map(i => i.name);
},
deselectAllInterfaces() {
this.selectedInterfaces = [];
},
async importSelectedInterfaces() {
if (!this.importFile) {
DialogUtils.alert("Please select a configuration file");
return;
}
if (this.selectedInterfaces.length === 0) {
DialogUtils.alert("Please select at least one interface to import");
return;
}
const formData = new FormData();
formData.append('config', this.importFile);
formData.append('selected_interfaces', JSON.stringify(this.selectedInterfaces));
try {
await window.axios.post('/api/v1/reticulum/interfaces/import', formData);
await this.loadInterfaces();
this.closeImportDialog();
DialogUtils.alert("Interfaces imported successfully");
} catch(e) {
const message = e.response?.data?.message || "Failed to import interfaces";
DialogUtils.alert(message);
console.error(e);
}
}
},
computed: {
isElectron() {