1
1
"use client" ;
2
- import { useEffect } from "react" ;
2
+ import { useEffect , useState } from "react" ;
3
3
import useSWR from "swr" ;
4
4
import { HostListing , bodiedFetcher , BASE_URL } from "./api" ;
5
5
import { useClientOnly } from "./hooks/useClientOnly" ;
@@ -25,6 +25,7 @@ export function GraphStats({
25
25
onAttributesLoaded ?: ( attributes : { [ key : string ] : string } ) => void ;
26
26
} ) {
27
27
const isClient = useClientOnly ( ) ;
28
+ const [ downloadingFormat , setDownloadingFormat ] = useState < string | null > ( null ) ;
28
29
29
30
// Fetch graph statistics and attributes.
30
31
// TODO: Perhaps these should all go in one combined query?
@@ -80,6 +81,9 @@ export function GraphStats({
80
81
* "graphml", "gml", "gexf", "json".
81
82
*/
82
83
function downloadGraph ( format : string = "graphml" ) {
84
+ // Set loading state
85
+ setDownloadingFormat ( format ) ;
86
+
83
87
// POST to /api/queries/graph/download with the "host_id" and "format"
84
88
// parameters in the body.
85
89
fetch ( `${ BASE_URL } /queries/graph/download` , {
@@ -101,6 +105,14 @@ export function GraphStats({
101
105
a . href = url ;
102
106
a . download = `${ graph . name } .${ format } ` ;
103
107
a . click ( ) ;
108
+ } )
109
+ . catch ( ( error ) => {
110
+ console . error ( "Download failed:" , error ) ;
111
+ // You could add error handling UI here if needed
112
+ } )
113
+ . finally ( ( ) => {
114
+ // Clear loading state
115
+ setDownloadingFormat ( null ) ;
104
116
} ) ;
105
117
}
106
118
@@ -158,14 +170,36 @@ export function GraphStats({
158
170
< div className = "flex gap-2" >
159
171
< button
160
172
onClick = { ( ) => downloadGraph ( "graphml" ) }
161
- className = "font-bold rounded text-white px-4 bg-blue-500 hover:bg-blue-700"
173
+ disabled = { downloadingFormat !== null }
174
+ className = { `font-bold rounded text-white px-4 py-2 flex items-center gap-2 ${
175
+ downloadingFormat === "graphml"
176
+ ? "bg-blue-400 cursor-not-allowed"
177
+ : "bg-blue-500 hover:bg-blue-700"
178
+ } `}
162
179
>
180
+ { downloadingFormat === "graphml" && (
181
+ < svg className = "animate-spin h-4 w-4 text-white" fill = "none" viewBox = "0 0 24 24" >
182
+ < circle className = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" strokeWidth = "4" > </ circle >
183
+ < path className = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" > </ path >
184
+ </ svg >
185
+ ) }
163
186
GraphML
164
187
</ button >
165
188
< button
166
189
onClick = { ( ) => downloadGraph ( "gexf" ) }
167
- className = "font-bold rounded text-white px-4 bg-blue-500 hover:bg-blue-700"
190
+ disabled = { downloadingFormat !== null }
191
+ className = { `font-bold rounded text-white px-4 py-2 flex items-center gap-2 ${
192
+ downloadingFormat === "gexf"
193
+ ? "bg-blue-400 cursor-not-allowed"
194
+ : "bg-blue-500 hover:bg-blue-700"
195
+ } `}
168
196
>
197
+ { downloadingFormat === "gexf" && (
198
+ < svg className = "animate-spin h-4 w-4 text-white" fill = "none" viewBox = "0 0 24 24" >
199
+ < circle className = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" strokeWidth = "4" > </ circle >
200
+ < path className = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" > </ path >
201
+ </ svg >
202
+ ) }
169
203
GEXF
170
204
</ button >
171
205
</ div >
0 commit comments