refactor importing interfaces to its own modal component
This commit is contained in:
149
src/frontend/components/interfaces/ImportInterfacesModal.vue
Normal file
149
src/frontend/components/interfaces/ImportInterfacesModal.vue
Normal 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>
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user