Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion website/src/components/AddDevice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ import AccordionDetails from '@mui/material/AccordionDetails';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import Box from '@mui/material/Box';
import { Warning } from '@mui/icons-material';
import { ImportExportDelete } from './ImportExportDelete';

interface Props {
onAdd: () => void;
onRefresh: () => void;
}

export const AddDevice = observer(
Expand Down Expand Up @@ -251,7 +253,9 @@ export const AddDevice = observer(
return (
<>
<Card>
<CardHeader title="Add A Device" />
<CardHeader title="Add A Device"
action={<ImportExportDelete onRefresh={() => this.props.onRefresh()} />}
/>
<CardContent>
<form onSubmit={this.submit}>
<FormControl fullWidth>
Expand Down
2 changes: 1 addition & 1 deletion website/src/components/Devices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const Devices = observer(
</Grid>
</Grid>
<Grid item xs={12} sm={10} md={10} lg={6}>
<AddDevice onAdd={() => this.devices.refresh()} />
<AddDevice onAdd={() => this.devices.refresh()} onRefresh={() => this.devices.refresh()} />
</Grid>
</Grid>
);
Expand Down
115 changes: 115 additions & 0 deletions website/src/components/ImportExportDelete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from 'react';
import { IconMenu } from './IconMenu';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import FileUploadIcon from '@mui/icons-material/FileUpload';
import FileDownloadIcon from '@mui/icons-material/FileDownload';
import DeleteIcon from '@mui/icons-material/Delete';
import { grpc } from '../Api';
import { toast } from './Toast';
import { confirm } from './Present';


export function ImportExportDelete({ onRefresh }: { onRefresh?: () => void }) {
const handleExport = async () => {
try {
const response = await grpc.devices.listDevices({});
const devices = response.items;
const jsonStr = JSON.stringify(devices, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'vpn-devices.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast({ text: 'Devices exported successfully', intent: 'success' });
} catch (error) {
toast({ text: 'Failed to export devices', intent: 'error' });
}
};

const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;

try {
const text = await file.text();
const devices = JSON.parse(text);

// Validate the imported data
if (!Array.isArray(devices)) {
throw new Error('Invalid format: expected an array of devices');
}

// Import each device
for (const device of devices) {
await grpc.devices.addDevice({
name: device.name,
publicKey: device.publicKey,
presharedKey: device.presharedKey || '',
manualIpAssignment: device.manualIpAssignment || false,
manualIpv4Address: device.manualIpv4Address || '',
manualIpv6Address: device.manualIpv6Address || '',
});
}

toast({ text: 'Devices imported successfully', intent: 'success' });
if (onRefresh) onRefresh();
} catch (error) {
toast({ text: 'Failed to import devices: ' + (error as Error).message, intent: 'error' });
}
};

const handleDeleteAll = async () => {
if (await confirm('Are you sure you want to delete ALL your devices? This action cannot be undone!')) {
try {
const response = await grpc.devices.listDevices({});
const devices = response.items;

for (const device of devices) {
await grpc.devices.deleteDevice({
name: device.name,
});
}

toast({ text: 'All devices deleted successfully', intent: 'success' });
if (onRefresh) onRefresh();
} catch (error) {
toast({ text: 'Failed to delete devices: ' + (error as Error).message, intent: 'error' });
}
}
};

return (
<IconMenu>
<MenuItem onClick={handleExport}>
<ListItemIcon>
<FileDownloadIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Export Devices</ListItemText>
</MenuItem>
<MenuItem component="label">
<ListItemIcon>
<FileUploadIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Import Devices</ListItemText>
<input
type="file"
hidden
accept=".json"
onChange={handleImport}
/>
</MenuItem>
<MenuItem onClick={handleDeleteAll}>
<ListItemIcon>
<DeleteIcon fontSize="small" />
</ListItemIcon>
<ListItemText>Delete All Devices</ListItemText>
</MenuItem>
</IconMenu>
);
}
Loading