migrate frontend to use new celery api and improve ux around spinners whilst loading
This commit is contained in:
parent
1ad8a12f3d
commit
9a164ddfdc
3 changed files with 135 additions and 76 deletions
|
|
@ -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<ParameterValues | null>(null);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const [alertDialogIsOpen, setAlertDialogIsOpen] = useState(false);
|
||||
const [spinnerText, setSpinnerText] = useState<string | null>(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() {
|
|||
<Parameters onSubmit={onSubmit} isOpen={isParametersModalOpen} setIsOpen={setIsParametersModalOpen} />
|
||||
<ActiveQuery taskID={taskID} />
|
||||
<AlertError message={submitError} open={alertDialogIsOpen} setIsOpen={setAlertDialogIsOpen} />
|
||||
|
||||
<Spinner show={spinnerText !== null} >
|
||||
<span >{spinnerText}</span>
|
||||
</Spinner>
|
||||
|
||||
</div>
|
||||
{Object.keys(listingData).length > 0 &&
|
||||
<div className="flex-1 w-full relative" style={{ minHeight: 0, marginBottom: '8rem' }}>
|
||||
|
|
@ -153,9 +164,6 @@ function App() {
|
|||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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<ModalProps> = ({
|
||||
taskID
|
||||
|
|
@ -71,50 +72,54 @@ const ActiveQuery: React.FC<ModalProps> = ({
|
|||
}, []);
|
||||
|
||||
const [progressPercentage, setProgressPercentage] = useState<number>(0);
|
||||
const [taskStatus, setTaskStatus] = useState<TaskStatus | null>(TaskStatus.QUEUED);
|
||||
const [taskStatus, setTaskStatus] = useState<TaskStatus | null>("PENDING");
|
||||
const [lastUpdateTime, setLastUpdateTime] = useState<Date>(new Date());
|
||||
const [fetchStatusError, setFetchStatusError] = useState<string | null>(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]);
|
||||
|
||||
|
|
|
|||
46
crawler/frontend/src/components/Spinner.tsx
Normal file
46
crawler/frontend/src/components/Spinner.tsx
Normal file
|
|
@ -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<typeof spinnerVariants>,
|
||||
VariantProps<typeof loaderVariants> {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Spinner({ size, show, children, className }: SpinnerContentProps) {
|
||||
return (
|
||||
<span className={spinnerVariants({ show })}>
|
||||
<Loader2 className={cn(loaderVariants({ size }), className)} />
|
||||
{/* <span className="text-red-400">Loading with custom style</span> */}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue