feat: add CSV import and export functionality for module entries. Implement endpoints for exporting and importing module entries in CSV format, supporting types AS_PREFIXES, DOMAINS, and IP_RANGES. Enhance UI with buttons for CSV operations, improving user experience in managing module data.
CI / changes (push) Successful in 6s
CI / openapi (push) Successful in 22s
CI / go (push) Successful in 50s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m4s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m5s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 15s
CI / docker-go-prime (push) Successful in 24s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m1s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m7s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m22s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m26s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m23s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m6s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m28s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m21s
CI / changes (push) Successful in 6s
CI / openapi (push) Successful in 22s
CI / go (push) Successful in 50s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m4s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m5s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 15s
CI / docker-go-prime (push) Successful in 24s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m1s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m7s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m22s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m26s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m23s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m6s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m28s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m21s
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { apiJSON, apiMutate } from '$lib/api/client.js';
|
||||
import { apiFetch, apiJSON, apiMutate } from '$lib/api/client.js';
|
||||
import type {
|
||||
ModuleRow,
|
||||
ModulePatch,
|
||||
@@ -72,6 +72,8 @@
|
||||
import Trash2 from '@lucide/svelte/icons/trash-2';
|
||||
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
||||
import Save from '@lucide/svelte/icons/save';
|
||||
import Upload from '@lucide/svelte/icons/upload';
|
||||
import Download from '@lucide/svelte/icons/download';
|
||||
|
||||
const moduleId = $derived(page.params.moduleId);
|
||||
|
||||
@@ -132,6 +134,28 @@
|
||||
|
||||
// Refresh
|
||||
let refreshing = $state(false);
|
||||
let csvImporting = $state(false);
|
||||
let csvExporting = $state(false);
|
||||
let csvFileInput = $state<HTMLInputElement | null>(null);
|
||||
|
||||
function supportsCsvIO(type: ModuleRow['type'] | null | undefined): boolean {
|
||||
return type === 'AS_PREFIXES' || type === 'DOMAINS' || type === 'IP_RANGES';
|
||||
}
|
||||
|
||||
function sanitizeFilenamePart(v: string): string {
|
||||
const cleaned = v
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9._-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^[-_.]+|[-_.]+$/g, '');
|
||||
return cleaned || 'module';
|
||||
}
|
||||
|
||||
async function readErrorText(res: Response): Promise<string> {
|
||||
const body = (await res.text()).trim();
|
||||
return body || `HTTP ${res.status}`;
|
||||
}
|
||||
|
||||
async function loadMod() {
|
||||
loadingMod = true;
|
||||
@@ -245,6 +269,67 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function exportEntriesCsv() {
|
||||
if (!mod || !supportsCsvIO(mod.type) || csvExporting) return;
|
||||
csvExporting = true;
|
||||
try {
|
||||
const res = await apiFetch(`/v1/modules/${moduleId}/entries.csv`, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'text/csv' }
|
||||
});
|
||||
if (!res.ok) {
|
||||
toast.error(await readErrorText(res));
|
||||
return;
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
const safeModuleName = sanitizeFilenamePart(mod.name);
|
||||
a.href = url;
|
||||
a.download = `${safeModuleName}-${mod.type.toLowerCase()}-entries.csv`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
csvExporting = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openImportCsvPicker() {
|
||||
if (!mod || !supportsCsvIO(mod.type) || csvImporting) return;
|
||||
csvFileInput?.click();
|
||||
}
|
||||
|
||||
async function handleImportCsvChange(event: Event) {
|
||||
const input = event.currentTarget as HTMLInputElement | null;
|
||||
const file = input?.files?.[0];
|
||||
if (!mod || !supportsCsvIO(mod.type) || !file || csvImporting) return;
|
||||
csvImporting = true;
|
||||
try {
|
||||
const fileText = await file.text();
|
||||
const res = await apiFetch(`/v1/modules/${moduleId}/entries.csv`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/csv' },
|
||||
body: fileText
|
||||
});
|
||||
if (!res.ok) {
|
||||
toast.error(await readErrorText(res));
|
||||
return;
|
||||
}
|
||||
const payload = (await res.json()) as { imported?: number };
|
||||
toast.success(`Импортировано записей: ${payload.imported ?? 0}`);
|
||||
await loadEntries();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
csvImporting = false;
|
||||
if (input) input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// --- AS Entries ---
|
||||
function openAsCreate() {
|
||||
asEdit = null;
|
||||
@@ -497,6 +582,13 @@
|
||||
<div class="text-muted-foreground flex h-32 items-center justify-center">Загрузка…</div>
|
||||
{:else if mod}
|
||||
<div class="space-y-6">
|
||||
<input
|
||||
class="hidden"
|
||||
type="file"
|
||||
accept=".csv,text/csv"
|
||||
bind:this={csvFileInput}
|
||||
onchange={handleImportCsvChange}
|
||||
/>
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
@@ -564,7 +656,27 @@
|
||||
Номер AS и community; имя, число префиксов и дата обновляются при успешном обновлении модуля (RIPEstat)
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button size="sm" class="shrink-0 self-start sm:self-auto" onclick={openAsCreate}><Plus />Добавить</Button>
|
||||
<div class="flex shrink-0 flex-wrap items-center justify-end gap-2 self-start sm:self-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={openImportCsvPicker}
|
||||
disabled={!supportsCsvIO(mod.type) || csvImporting || csvExporting}
|
||||
>
|
||||
<Upload />
|
||||
{csvImporting ? 'Импорт…' : 'Импорт CSV'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={exportEntriesCsv}
|
||||
disabled={!supportsCsvIO(mod.type) || csvExporting || csvImporting}
|
||||
>
|
||||
<Download />
|
||||
{csvExporting ? 'Экспорт…' : 'Экспорт CSV'}
|
||||
</Button>
|
||||
<Button size="sm" onclick={openAsCreate}><Plus />Добавить</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="p-0">
|
||||
<Table>
|
||||
@@ -672,7 +784,27 @@
|
||||
<CardTitle class="text-base">Домены</CardTitle>
|
||||
<CardDescription>FQDN для резолвинга через DoH</CardDescription>
|
||||
</div>
|
||||
<Button size="sm" class="shrink-0 self-start sm:self-auto" onclick={openDomainCreate}><Plus />Добавить</Button>
|
||||
<div class="flex shrink-0 flex-wrap items-center justify-end gap-2 self-start sm:self-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={openImportCsvPicker}
|
||||
disabled={!supportsCsvIO(mod.type) || csvImporting || csvExporting}
|
||||
>
|
||||
<Upload />
|
||||
{csvImporting ? 'Импорт…' : 'Импорт CSV'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={exportEntriesCsv}
|
||||
disabled={!supportsCsvIO(mod.type) || csvExporting || csvImporting}
|
||||
>
|
||||
<Download />
|
||||
{csvExporting ? 'Экспорт…' : 'Экспорт CSV'}
|
||||
</Button>
|
||||
<Button size="sm" onclick={openDomainCreate}><Plus />Добавить</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="p-0">
|
||||
<Table>
|
||||
@@ -711,7 +843,27 @@
|
||||
<CardTitle class="text-base">IP-диапазоны</CardTitle>
|
||||
<CardDescription>Статические CIDR для анонса</CardDescription>
|
||||
</div>
|
||||
<Button size="sm" class="shrink-0 self-start sm:self-auto" onclick={openIpCreate}><Plus />Добавить</Button>
|
||||
<div class="flex shrink-0 flex-wrap items-center justify-end gap-2 self-start sm:self-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={openImportCsvPicker}
|
||||
disabled={!supportsCsvIO(mod.type) || csvImporting || csvExporting}
|
||||
>
|
||||
<Upload />
|
||||
{csvImporting ? 'Импорт…' : 'Импорт CSV'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={exportEntriesCsv}
|
||||
disabled={!supportsCsvIO(mod.type) || csvExporting || csvImporting}
|
||||
>
|
||||
<Download />
|
||||
{csvExporting ? 'Экспорт…' : 'Экспорт CSV'}
|
||||
</Button>
|
||||
<Button size="sm" onclick={openIpCreate}><Plus />Добавить</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="p-0">
|
||||
<Table>
|
||||
|
||||
Reference in New Issue
Block a user