feat: Add CSV download functionality for filtered dataset entries
This commit is contained in:
53
src/app/api/download-metadata-csv/route.ts
Normal file
53
src/app/api/download-metadata-csv/route.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { downloadFilteredEntries } from '@/app/actions/download-filtered-entries';
|
||||||
|
import { requireAdmin } from '@/lib/auth';
|
||||||
|
|
||||||
|
interface FilterParams {
|
||||||
|
speakerId?: string;
|
||||||
|
modelName?: string;
|
||||||
|
dialect?: string;
|
||||||
|
iteration?: number;
|
||||||
|
utteranceId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const resultAdmin = await requireAdmin();
|
||||||
|
if (!resultAdmin.authenticated || !resultAdmin.admin) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Unauthorized' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { datasetId, filters } = body;
|
||||||
|
|
||||||
|
if (!datasetId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'datasetId is required' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filtered entries data from server action
|
||||||
|
const result = await downloadFilteredEntries(datasetId, filters || {});
|
||||||
|
|
||||||
|
const fileName = `dataset-${datasetId}-metadata-${new Date().toISOString().split('T')[0]}.csv`;
|
||||||
|
|
||||||
|
return new NextResponse(result.csvContent, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/csv',
|
||||||
|
'Content-Disposition': `attachment; filename="${fileName}"`,
|
||||||
|
'Content-Length': Buffer.byteLength(result.csvContent).toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error downloading CSV:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to download CSV file' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import Link from 'next/link';
|
|||||||
import AudioPlayer from './AudioPlayer';
|
import AudioPlayer from './AudioPlayer';
|
||||||
import { getDatasetEntries } from '@/app/actions/get-dataset-entries';
|
import { getDatasetEntries } from '@/app/actions/get-dataset-entries';
|
||||||
import { getFilterOptions } from '@/app/actions/get-filter-options';
|
import { getFilterOptions } from '@/app/actions/get-filter-options';
|
||||||
import { downloadFilteredEntriesAsZip } from '@/lib/download-utils';
|
import { downloadFilteredEntriesAsZip, downloadFilteredEntriesAsCsv } from '@/lib/download-utils';
|
||||||
|
|
||||||
interface DatasetEntriesListProps {
|
interface DatasetEntriesListProps {
|
||||||
datasetId: number;
|
datasetId: number;
|
||||||
@@ -45,6 +45,7 @@ export default function DatasetEntriesList({
|
|||||||
const [entries, setEntries] = useState<DatasetEntry[]>([]);
|
const [entries, setEntries] = useState<DatasetEntry[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [downloading, setDownloading] = useState(false);
|
const [downloading, setDownloading] = useState(false);
|
||||||
|
const [csvDownloading, setCsvDownloading] = useState(false);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
@@ -197,6 +198,26 @@ export default function DatasetEntriesList({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDownloadFilteredCsv() {
|
||||||
|
setCsvDownloading(true);
|
||||||
|
try {
|
||||||
|
const filterParams = {
|
||||||
|
speakerId: filters.speakerId || undefined,
|
||||||
|
modelName: filters.modelName || undefined,
|
||||||
|
dialect: filters.dialect || undefined,
|
||||||
|
iteration: filters.iteration ? parseInt(filters.iteration, 10) : undefined,
|
||||||
|
utteranceId: filters.utteranceId || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
await downloadFilteredEntriesAsCsv(datasetId, filterParams);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error downloading CSV:', error);
|
||||||
|
alert('Failed to download CSV. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setCsvDownloading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (entries.length === 0 && !loading) {
|
if (entries.length === 0 && !loading) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
@@ -207,6 +228,13 @@ export default function DatasetEntriesList({
|
|||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-lg font-semibold text-gray-700">Filters</h3>
|
<h3 className="text-lg font-semibold text-gray-700">Filters</h3>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={handleDownloadFilteredCsv}
|
||||||
|
disabled={csvDownloading || total === 0}
|
||||||
|
className="text-sm px-3 py-1 bg-purple-500 text-white rounded hover:bg-purple-600 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{csvDownloading ? 'Downloading...' : 'Download CSV'}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDownloadFiltered}
|
onClick={handleDownloadFiltered}
|
||||||
disabled={downloading || total === 0}
|
disabled={downloading || total === 0}
|
||||||
@@ -325,6 +353,13 @@ export default function DatasetEntriesList({
|
|||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-lg font-semibold text-gray-700">Filters</h3>
|
<h3 className="text-lg font-semibold text-gray-700">Filters</h3>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={handleDownloadFilteredCsv}
|
||||||
|
disabled={csvDownloading || total === 0}
|
||||||
|
className="text-sm px-3 py-1 bg-purple-500 text-white rounded hover:bg-purple-600 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{csvDownloading ? 'Downloading...' : 'Download CSV'}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDownloadFiltered}
|
onClick={handleDownloadFiltered}
|
||||||
disabled={downloading || total === 0}
|
disabled={downloading || total === 0}
|
||||||
|
|||||||
@@ -44,3 +44,42 @@ export async function downloadFilteredEntriesAsZip(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function downloadFilteredEntriesAsCsv(
|
||||||
|
datasetId: number,
|
||||||
|
filters: FilterParams
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/download-metadata-csv', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
datasetId,
|
||||||
|
filters,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || 'Failed to download CSV');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the CSV blob from response
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
// Trigger download
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `dataset-${datasetId}-metadata-${new Date().toISOString().split('T')[0]}.csv`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error downloading CSV:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user