Files
experiments-database/frontend/src/components/ExportDataModal.jsx
T

113 lines
4.1 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useMemo, useState } from 'react';
import { experimentsApi } from '../api/client';
import Button from './ui/Button';
import Alert from './ui/Alert';
import { listExportableParams, buildMatrix, toCSV, csvFilename } from '../lib/dataExport';
const GROUP_LABELS = { metrics: 'Session metrics', daily: 'Daily record fields' };
function triggerDownload(filename, text) {
const blob = new Blob([text], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 0);
}
export default function ExportDataModal({ experimentTitle, experimentId, dailyTemplate, animals, onClose }) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [statuses, setStatuses] = useState([]);
const [selectedId, setSelectedId] = useState(null);
useEffect(() => {
let alive = true;
setLoading(true);
setError(null);
experimentsApi.getDailyStatuses(experimentId, {})
.then((data) => { if (alive) setStatuses(data); })
.catch((err) => { if (alive) setError(err.message); })
.finally(() => { if (alive) setLoading(false); });
return () => { alive = false; };
}, [experimentId]);
const params = useMemo(() => listExportableParams(dailyTemplate, statuses), [dailyTemplate, statuses]);
// Derive the effective selection: the user's pick, else the first parameter.
const effectiveId = selectedId ?? params[0]?.id ?? null;
const selectedParam = params.find((p) => p.id === effectiveId) ?? null;
const matrix = useMemo(
() => (selectedParam ? buildMatrix(statuses, animals, selectedParam) : { columns: [], rows: [] }),
[statuses, animals, selectedParam],
);
const hasData = matrix.rows.length > 0;
// Group params for <optgroup> rendering, preserving order.
const grouped = useMemo(() => {
const out = [];
for (const p of params) {
let g = out.find((x) => x.group === p.group);
if (!g) { g = { group: p.group, items: [] }; out.push(g); }
g.items.push(p);
}
return out;
}, [params]);
function handleExport() {
if (!hasData || !selectedParam) return;
triggerDownload(csvFilename(experimentTitle, selectedParam.label), toCSV(matrix));
onClose();
}
if (loading) return <p className="text-sm text-gray-400" aria-live="polite">Loading data</p>;
if (error) return (
<div className="space-y-4">
<Alert type="error" message={error} />
<div className="flex justify-end">
<Button variant="secondary" onClick={onClose}>Close</Button>
</div>
</div>
);
return (
<div className="space-y-4">
<p className="text-sm text-gray-500">
Export one parameter as a CSV file each row is a date, each column is a subject.
</p>
<div>
<label htmlFor="export-param" className="block text-xs font-medium text-gray-500 mb-1">Parameter</label>
<select
id="export-param"
value={effectiveId ?? ''}
onChange={(e) => setSelectedId(e.target.value)}
className="w-full border border-gray-200 rounded px-2 py-1.5 text-sm bg-white focus:outline-none focus:ring-1 focus:ring-indigo-400"
>
{grouped.map((g) => (
<optgroup key={g.group} label={GROUP_LABELS[g.group] ?? g.group}>
{g.items.map((p) => <option key={p.id} value={p.id}>{p.label}</option>)}
</optgroup>
))}
</select>
</div>
<p className="text-xs text-gray-400">
{hasData
? `${matrix.rows.length} date${matrix.rows.length !== 1 ? 's' : ''} × ${matrix.columns.length} subject${matrix.columns.length !== 1 ? 's' : ''}`
: 'No data for this parameter yet.'}
</p>
<div className="flex justify-end gap-2 pt-2">
<Button variant="secondary" onClick={onClose}>Cancel</Button>
<Button onClick={handleExport} disabled={!hasData}>Export CSV</Button>
</div>
</div>
);
}