migrate frontend to use new celery api and improve ux around spinners whilst loading

This commit is contained in:
Viktor Barzin 2025-06-22 21:20:42 +00:00
parent 1ad8a12f3d
commit 9a164ddfdc
No known key found for this signature in database
GPG key ID: 4056458DBDBF8863
3 changed files with 135 additions and 76 deletions

View file

@ -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>
</>

View file

@ -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]);

View 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>
);
}