Skip to content

Commit 5cf7713

Browse files
Tiya-VermaMaddieWright
authored andcommitted
Export & Import task
1 parent ffe88b0 commit 5cf7713

File tree

9 files changed

+575
-18
lines changed

9 files changed

+575
-18
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { forwardToBackend } from '@/lib/backend-proxy';
2+
import { NextRequest } from 'next/server';
3+
4+
export async function POST(
5+
req: NextRequest,
6+
{ params }: { params: { session_id: string } }
7+
) {
8+
const body = await req.text();
9+
10+
const response = await forwardToBackend({
11+
method: 'POST',
12+
path: `/api/sessions/${params.session_id}/eeg_data/export`,
13+
body,
14+
contentType: 'application/json',
15+
});
16+
17+
// Return the CSV as plain text
18+
const text = await response.text();
19+
return new Response(text, {
20+
status: response.status,
21+
headers: {
22+
'Content-Type': response.headers.get('Content-Type') ?? 'text/csv',
23+
},
24+
});
25+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { NextRequest } from 'next/server';
2+
3+
const DEFAULT_API_BASES = [
4+
process.env.SESSION_API_BASE_URL,
5+
process.env.API_BASE_URL,
6+
process.env.VITE_API_URL,
7+
'http://api-server:9000',
8+
'http://127.0.0.1:9000',
9+
'http://localhost:9000',
10+
].filter((v): v is string => Boolean(v));
11+
12+
export async function POST(
13+
req: NextRequest,
14+
{ params }: { params: { session_id: string } }
15+
) {
16+
const csvBody = await req.text();
17+
const path = `/api/sessions/${params.session_id}/eeg_data/import`;
18+
19+
let lastError: unknown = null;
20+
for (const baseUrl of DEFAULT_API_BASES) {
21+
const url = `${baseUrl.replace(/\/$/, '')}${path}`;
22+
try {
23+
const backendResp = await fetch(url, {
24+
method: 'POST',
25+
headers: { 'Content-Type': 'text/csv' },
26+
body: csvBody,
27+
cache: 'no-store',
28+
});
29+
const text = await backendResp.text();
30+
return new Response(text, {
31+
status: backendResp.status,
32+
headers: {
33+
'Content-Type':
34+
backendResp.headers.get('Content-Type') ??
35+
'application/json',
36+
},
37+
});
38+
} catch (error) {
39+
lastError = error;
40+
}
41+
}
42+
43+
const fallbackMessage =
44+
lastError instanceof Error ? lastError.message : 'Unknown error';
45+
46+
return new Response(
47+
JSON.stringify({
48+
message: `Could not reach API backend: ${fallbackMessage}`,
49+
}),
50+
{
51+
status: 503,
52+
headers: { 'Content-Type': 'application/json' },
53+
}
54+
);
55+
}

frontend/components/nodes/signal-graph-node/signal-graph-node.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { Card } from '@/components/ui/card';
22
import { Handle, Position, useReactFlow } from '@xyflow/react';
33
import { useGlobalContext } from '@/context/GlobalContext';
44
import useNodeData from '@/hooks/useNodeData';
5-
import { ArrowUpRight } from 'lucide-react';
6-
import React from 'react';
5+
import { ArrowUpRight, Download } from 'lucide-react';
6+
import React, { useState } from 'react';
77

88
import {
99
Dialog,
@@ -14,6 +14,7 @@ import {
1414
DialogTrigger,
1515
} from '@/components/ui/dialog';
1616
import SignalGraphView from './signal-graph-full';
17+
import ExportDialog from '@/components/ui/export-dialog';
1718

1819
export default function SignalGraphNode({ id }: { id?: string }) {
1920
const { dataStreaming } = useGlobalContext();
@@ -22,6 +23,8 @@ export default function SignalGraphNode({ id }: { id?: string }) {
2223
const processedData = renderData;
2324
const reactFlowInstance = useReactFlow();
2425
const [isConnected, setIsConnected] = React.useState(false);
26+
const { activeSessionId } = useGlobalContext();
27+
const [isExportOpen, setIsExportOpen] = useState(false);
2528

2629
// Determine if this Chart View node has an upstream path from a Source
2730
const checkConnectionStatus = React.useCallback(() => {
@@ -98,14 +101,23 @@ export default function SignalGraphNode({ id }: { id?: string }) {
98101
Chart View
99102
</span>
100103
{isConnected && (
101-
<div className="w-full mt-[50px] transition-all duration-300 ease-in-out">
104+
<div className="w-full mt-[50px] transition-all duration-300 ease-in-out flex items-center gap-3">
102105
<DialogTrigger asChild>
103106
<button
104107
className="font-geist text-[14px] font-normal leading-tight text-black flex items-center gap-1 hover:opacity-80 transition"
105108
onClick={(e) => e.stopPropagation()}>
106109
Preview <ArrowUpRight size={14} className="transition-transform duration-200 hover:scale-110" />
107110
</button>
108111
</DialogTrigger>
112+
<button
113+
className="font-geist text-[14px] font-normal leading-tight text-black flex items-center gap-1 hover:opacity-80 transition"
114+
onClick={(e) => {
115+
e.stopPropagation();
116+
setIsExportOpen(true);
117+
}}
118+
>
119+
Export <Download size={14} className="transition-transform duration-200 hover:scale-110" />
120+
</button>
109121
</div>
110122
)}
111123
</div>
@@ -122,9 +134,9 @@ export default function SignalGraphNode({ id }: { id?: string }) {
122134
</div>
123135

124136

125-
<DialogContent
126-
className="items-center justify-center w-screen h-screen max-w-none max-h-none"
127-
style={{ backgroundColor : '#EAF1F0'}}
137+
<DialogContent
138+
className="items-center justify-center w-screen h-screen max-w-none max-h-none"
139+
style={{ backgroundColor: '#EAF1F0' }}
128140
>
129141
<DialogHeader>
130142
<DialogTitle></DialogTitle>
@@ -135,6 +147,13 @@ export default function SignalGraphNode({ id }: { id?: string }) {
135147
</div>
136148
</DialogContent>
137149
</Card>
150+
151+
{/* Export dialog — outside the ReactFlow Dialog to avoid nesting */}
152+
<ExportDialog
153+
open={isExportOpen}
154+
sessionId={activeSessionId}
155+
onOpenChange={setIsExportOpen}
156+
/>
138157
</Dialog>
139158
);
140159
}

frontend/components/ui-header/app-header.tsx

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@ import {
1111
} from '@/components/ui/dropdown-menu';
1212
import { useState } from 'react';
1313
import Image from 'next/image';
14+
import { useGlobalContext } from '@/context/GlobalContext';
15+
import ExportDialog from '@/components/ui/export-dialog';
16+
import ImportDialog from '@/components/ui/import-dialog';
1417

1518
export default function AppHeader() {
1619
const [isOpen, setIsOpen] = useState<boolean>(false);
20+
const [isExportOpen, setIsExportOpen] = useState(false);
21+
const [isImportOpen, setIsImportOpen] = useState(false);
22+
const { activeSessionId } = useGlobalContext();
1723

1824
return (
1925
<header className="flex justify-between items-center p-2 border-b h-25">
@@ -28,21 +34,41 @@ export default function AppHeader() {
2834
/>
2935
</div>
3036

31-
{/* update, issues */}
37+
{/* update, issues, import, export, help */}
3238
<div className="flex h-full items-center space-x-4">
3339
<Button
3440
variant="link"
3541
className="flex items-center space-x-1 px-3 py-2"
3642
>
3743
<span>Update</span>
38-
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px'}} />
44+
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px' }} />
3945
</Button>
4046
<Button
4147
variant="link"
4248
className="flex items-center space-x-1 px-3 py-2"
4349
>
4450
<span>Issues</span>
45-
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px'}} />
51+
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px' }} />
52+
</Button>
53+
54+
{/* Export Data */}
55+
<Button
56+
variant="link"
57+
className="flex items-center space-x-1 px-3 py-2"
58+
onClick={() => setIsExportOpen(true)}
59+
>
60+
<span>Export Data</span>
61+
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px' }} />
62+
</Button>
63+
64+
{/* Import Data */}
65+
<Button
66+
variant="link"
67+
className="flex items-center space-x-1 px-3 py-2"
68+
onClick={() => setIsImportOpen(true)}
69+
>
70+
<span>Import Data</span>
71+
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px' }} />
4672
</Button>
4773

4874
{/* help */}
@@ -53,9 +79,8 @@ export default function AppHeader() {
5379
Help
5480
</span>
5581
<ChevronUpIcon
56-
className={`h-4 w-4 transform transition-transform duration-300 ${
57-
isOpen ? 'rotate-180' : 'rotate-0'
58-
}`}
82+
className={`h-4 w-4 transform transition-transform duration-300 ${isOpen ? 'rotate-180' : 'rotate-0'
83+
}`}
5984
/>
6085
</div>
6186
</DropdownMenuTrigger>
@@ -69,6 +94,18 @@ export default function AppHeader() {
6994
</DropdownMenuContent>
7095
</DropdownMenu>
7196
</div>
97+
98+
{/* Dialogs */}
99+
<ExportDialog
100+
open={isExportOpen}
101+
sessionId={activeSessionId}
102+
onOpenChange={setIsExportOpen}
103+
/>
104+
<ImportDialog
105+
open={isImportOpen}
106+
sessionId={activeSessionId}
107+
onOpenChange={setIsImportOpen}
108+
/>
72109
</header>
73110
);
74111
}

frontend/components/ui-header/settings-bar.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ export default function SettingsBar() {
320320
</div>
321321

322322

323-
{/* start/stop, reset, save, load */}
323+
{/* start/stop, reset, import, export, save, load */}
324324
<div className="flex space-x-2">
325325
<Button
326326
onClick={handleStartStop}
@@ -349,14 +349,15 @@ export default function SettingsBar() {
349349
</Dialog>
350350
<Button
351351
variant="outline"
352+
352353
onClick={handleSaveClick}
353354
disabled={isSaving || isLoading || isFetchingSessions}
354355
>
355356
{isSaving
356357
? 'Saving...'
357358
: fetchingFor === 'save'
358-
? 'Preparing...'
359-
: 'Save'}
359+
? 'Preparing...'
360+
: 'Save'}
360361
</Button>
361362
<Button
362363
variant="outline"
@@ -366,8 +367,8 @@ export default function SettingsBar() {
366367
{isLoading
367368
? 'Loading...'
368369
: fetchingFor === 'load'
369-
? 'Preparing...'
370-
: 'Load'}
370+
? 'Preparing...'
371+
: 'Load'}
371372
</Button>
372373
</div>
373374

0 commit comments

Comments
 (0)