diff --git a/crawler/frontend/src/App.tsx b/crawler/frontend/src/App.tsx index 9261836..12ca163 100644 --- a/crawler/frontend/src/App.tsx +++ b/crawler/frontend/src/App.tsx @@ -8,6 +8,7 @@ import AlertError from './components/AlertError'; import LoginModal from './components/LoginModal'; import { Map } from './components/Map'; import { Parameters, type ParameterValues } from './components/Parameters'; +import { Spinner } from './components/Spinner'; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from './components/ui/breadcrumb'; import { Button } from './components/ui/button'; import { Separator } from './components/ui/separator'; @@ -64,6 +65,7 @@ function App() { const [queryParameters, setQueryParameters] = useState(null); const [submitError, setSubmitError] = useState(null); const [alertDialogIsOpen, setAlertDialogIsOpen] = useState(false); + const [spinnerText, setSpinnerText] = useState(null); useEffect(() => { // Check if this is a callback from Authentik (after login) @@ -88,27 +90,31 @@ function App() { setIsParametersModalOpen(false) let data = null; if (action === 'visualize') { + setSpinnerText("Loading data for visualization...") try { data = await fetchData(user, "/api/listing_geojson", parameters); } catch (error) { // @ts-expect-error setSubmitError(error.message) setAlertDialogIsOpen(true) + } finally { + setSpinnerText(null) } if (data) { setListingData(data); } } else if (action === 'fetch-data') { + setSpinnerText("Submitting query to refresh listings...") try { data = await fetchData(user, "/api/refresh_listings", parameters, 'POST'); + // @ts-expect-error + setTaskID(data.task_id) } catch (error) { // @ts-expect-error setSubmitError(error.message) setAlertDialogIsOpen(true) - } - if (data) { - // @ts-expect-error - setTaskID(data.task_id) + } finally { + setSpinnerText(null) } } console.log(data) @@ -146,6 +152,11 @@ function App() { + + + {spinnerText} + + {Object.keys(listingData).length > 0 &&
@@ -153,9 +164,6 @@ function App() {
} - - - diff --git a/crawler/frontend/src/components/ActiveQuery.tsx b/crawler/frontend/src/components/ActiveQuery.tsx index 9830ecf..9f5a099 100644 --- a/crawler/frontend/src/components/ActiveQuery.tsx +++ b/crawler/frontend/src/components/ActiveQuery.tsx @@ -9,7 +9,7 @@ interface ModalProps { taskID: string | null; } -const fetchTaskStatus = async (user: User, taskID: string) => { +const fetchTaskStatusData = async (user: User, taskID: string) => { const accessToken = user?.access_token; const response = await fetch(`/api/task_status?task_id=${taskID}`, { method: 'GET', @@ -27,40 +27,41 @@ const fetchTaskStatus = async (user: User, taskID: string) => { return data; }; -enum TaskStatus { - QUEUED = 'queued', - PROCESSING = 'processing', - COMPLETED = 'completed', - FAILED = 'failed', -} +type TaskStatus = string +// enum TaskStatus { +// QUEUED = 'queued', +// PROCESSING = 'processing', +// COMPLETED = 'completed', +// FAILED = 'failed', +// } -const taskStatusToProgress = (taskStatus: TaskStatus): number => { - switch (taskStatus) { - case TaskStatus.QUEUED: - return 0.33; // Queued status - case TaskStatus.PROCESSING: - return 0.66; // Processing status - case TaskStatus.COMPLETED: - return 1.0; // Completed status - default: - throw new Error('Unknown task status: ' + status); - } -} +// const taskStatusToProgress = (taskStatus: TaskStatus): number => { +// switch (taskStatus) { +// case TaskStatus.QUEUED: +// return 0.33; // Queued status +// case TaskStatus.PROCESSING: +// return 0.66; // Processing status +// case TaskStatus.COMPLETED: +// return 1.0; // Completed status +// default: +// throw new Error('Unknown task status: ' + status); +// } +// } -const getTaskStatus = (status: string): TaskStatus => { - switch (status.toLowerCase()) { - case 'queued': - return TaskStatus.QUEUED; - case 'processing': - return TaskStatus.PROCESSING; - case 'completed': - return TaskStatus.COMPLETED; - case 'failed': - return TaskStatus.FAILED; - default: - throw new Error('Unknown task status: ' + status); - } -}; +// const getTaskStatus = (status: string): TaskStatus => { +// switch (status.toLowerCase()) { +// case 'queued': +// return TaskStatus.QUEUED; +// case 'processing': +// return TaskStatus.PROCESSING; +// case 'completed': +// return TaskStatus.COMPLETED; +// case 'failed': +// return TaskStatus.FAILED; +// default: +// throw new Error('Unknown task status: ' + status); +// } +// }; const ActiveQuery: React.FC = ({ taskID @@ -71,50 +72,54 @@ const ActiveQuery: React.FC = ({ }, []); const [progressPercentage, setProgressPercentage] = useState(0); - const [taskStatus, setTaskStatus] = useState(TaskStatus.QUEUED); + const [taskStatus, setTaskStatus] = useState("PENDING"); const [lastUpdateTime, setLastUpdateTime] = useState(new Date()); const [fetchStatusError, setFetchStatusError] = useState(null); const [alertDialogIsOpen, setAlertDialogIsOpen] = useState(false); + const fetchTaskStatus = async (interval: NodeJS.Timeout) => { + if (!user || !taskID) { + return; + } + let data = null + try { + data = await fetchTaskStatusData(user, taskID); + } catch (error: any) { + clearInterval(interval); + setTaskStatus("FAILURE") + setAlertDialogIsOpen(true) + if (error instanceof Error) { + setFetchStatusError(error.message) + } else { + setFetchStatusError('Failed to update task status: ' + error.toString()) + } + } + if (!data) { + clearInterval(interval); + return; + } + setLastUpdateTime(new Date()); + // const taskStatus = getTaskStatus(data.status); + const taskStatus = data.status; + if (taskStatus === "FAILURE") { + clearInterval(interval); + throw new Error('Task failed'); + } + setTaskStatus(taskStatus); + // const progress = taskStatusToProgress(taskStatus); + const parsedResult = JSON.parse(data.result) + setProgressPercentage(parsedResult.progress * 100); + if (taskStatus === "SUCCESS") { + clearInterval(interval); + return; + } + }; + // fetch status periodically // maybe move to ws one day useEffect(() => { const interval = setInterval - (async () => { - if (!user || !taskID) { - return; - } - let data = null - try { - data = await fetchTaskStatus(user, taskID); - } catch (error: any) { - clearInterval(interval); - setTaskStatus(TaskStatus.FAILED) - setAlertDialogIsOpen(true) - if (error instanceof Error) { - setFetchStatusError(error.message) - } else { - setFetchStatusError('Failed to update task status: ' + error.toString()) - } - } - if (!data) { - clearInterval(interval); - return; - } - setLastUpdateTime(new Date()); - const taskStatus = getTaskStatus(data.status); - if (taskStatus === TaskStatus.FAILED) { - clearInterval(interval); - throw new Error('Task failed'); - } - setTaskStatus(taskStatus); - const progress = taskStatusToProgress(taskStatus); - setProgressPercentage(progress * 100); - if (taskStatus === TaskStatus.COMPLETED) { - clearInterval(interval); - return; - } - }, 5000); // every 5 seconds + (() => fetchTaskStatus(interval), 5000); // every 5 seconds return () => clearInterval(interval); }, [taskID]); diff --git a/crawler/frontend/src/components/Spinner.tsx b/crawler/frontend/src/components/Spinner.tsx new file mode 100644 index 0000000..019c071 --- /dev/null +++ b/crawler/frontend/src/components/Spinner.tsx @@ -0,0 +1,46 @@ +import { cn } from '@/lib/utils'; +import { type VariantProps, cva } from 'class-variance-authority'; +import { Loader2 } from 'lucide-react'; +import React from 'react'; + +const spinnerVariants = cva('flex-col items-center justify-center', { + variants: { + show: { + true: 'flex', + false: 'hidden', + }, + }, + defaultVariants: { + show: true, + }, +}); + +const loaderVariants = cva('animate-spin text-primary', { + variants: { + size: { + small: 'size-6', + medium: 'size-8', + large: 'size-12', + }, + }, + defaultVariants: { + size: 'medium', + }, +}); + +interface SpinnerContentProps + extends VariantProps, + VariantProps { + className?: string; + children?: React.ReactNode; +} + +export function Spinner({ size, show, children, className }: SpinnerContentProps) { + return ( + + + {/* Loading with custom style */} + {children} + + ); +}