Improve job status notifications
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
@@ -17,45 +18,46 @@ function isTerminalTask(task: Task) {
|
||||
}
|
||||
|
||||
function taskSignature(task: Task) {
|
||||
return `${task.status}|${task.stage}|${task.stage_detail ?? ''}|${task.error ?? ''}`;
|
||||
return JSON.stringify({
|
||||
status: task.status,
|
||||
stage: task.stage,
|
||||
stageDetail: task.stage_detail,
|
||||
stageContext: task.stage_context,
|
||||
error: task.error,
|
||||
result: isTerminalTask(task) ? task.result : null
|
||||
});
|
||||
}
|
||||
|
||||
function taskTitle(task: Task) {
|
||||
switch (task.task_type) {
|
||||
case 'sync_filings':
|
||||
return 'Filing sync';
|
||||
case 'refresh_prices':
|
||||
return 'Price refresh';
|
||||
case 'analyze_filing':
|
||||
return 'Filing analysis';
|
||||
case 'portfolio_insights':
|
||||
return 'Portfolio insight';
|
||||
default:
|
||||
return 'Task';
|
||||
function taskProgressLabel(task: Task) {
|
||||
const progress = task.notification.progress;
|
||||
if (!progress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${progress.current}/${progress.total} ${progress.unit}`;
|
||||
}
|
||||
|
||||
function taskDescription(task: Task) {
|
||||
if (task.error && task.status === 'failed') {
|
||||
return task.error;
|
||||
}
|
||||
const lines = [
|
||||
task.notification.statusLine,
|
||||
task.notification.detailLine,
|
||||
taskProgressLabel(task)
|
||||
].filter((value): value is string => Boolean(value));
|
||||
|
||||
if (task.stage_detail) {
|
||||
return task.stage_detail;
|
||||
}
|
||||
return lines.join(' • ');
|
||||
}
|
||||
|
||||
switch (task.status) {
|
||||
case 'queued':
|
||||
return 'Queued and waiting for execution.';
|
||||
case 'running':
|
||||
return 'Running in workflow engine.';
|
||||
case 'completed':
|
||||
return 'Task finished successfully.';
|
||||
case 'failed':
|
||||
return 'Task failed.';
|
||||
default:
|
||||
return 'Task status changed.';
|
||||
}
|
||||
function taskTitle(task: Task) {
|
||||
return task.notification.title;
|
||||
}
|
||||
|
||||
function terminalToastDescription(task: Task) {
|
||||
const topStat = task.notification.stats[0];
|
||||
return [
|
||||
task.notification.statusLine,
|
||||
topStat ? `${topStat.label}: ${topStat.value}` : null,
|
||||
task.notification.detailLine
|
||||
].filter((value): value is string => Boolean(value)).join(' • ');
|
||||
}
|
||||
|
||||
function shouldNotifyTask(task: Task) {
|
||||
@@ -82,12 +84,14 @@ type UseTaskNotificationsCenterResult = {
|
||||
isDetailOpen: boolean;
|
||||
setIsDetailOpen: (value: boolean) => void;
|
||||
openTaskDetails: (taskId: string) => void;
|
||||
openTaskAction: (task: Task, actionId?: string | null) => void;
|
||||
markTaskRead: (taskId: string, read?: boolean) => Promise<void>;
|
||||
silenceTask: (taskId: string, silenced?: boolean) => Promise<void>;
|
||||
refreshTasks: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function useTaskNotificationsCenter(): UseTaskNotificationsCenterResult {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const [activeTasks, setActiveTasks] = useState<Task[]>([]);
|
||||
const [finishedTasks, setFinishedTasks] = useState<Task[]>([]);
|
||||
@@ -159,6 +163,22 @@ export function useTaskNotificationsCenter(): UseTaskNotificationsCenterResult {
|
||||
setIsPopoverOpen(false);
|
||||
}, []);
|
||||
|
||||
const openTaskAction = useCallback((task: Task, actionId?: string | null) => {
|
||||
const action = actionId
|
||||
? task.notification.actions.find((entry) => entry.id === actionId)
|
||||
: task.notification.actions.find((entry) => entry.primary && entry.id !== 'open_details')
|
||||
?? task.notification.actions.find((entry) => entry.id !== 'open_details')
|
||||
?? null;
|
||||
|
||||
if (!action || action.id === 'open_details' || !action.href) {
|
||||
openTaskDetails(task.id);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPopoverOpen(false);
|
||||
router.push(action.href);
|
||||
}, [openTaskDetails, router]);
|
||||
|
||||
const silenceTask = useCallback(async (taskId: string, silenced = true) => {
|
||||
try {
|
||||
const { task } = await updateTaskNotificationState(taskId, { silenced });
|
||||
@@ -207,14 +227,24 @@ export function useTaskNotificationsCenter(): UseTaskNotificationsCenterResult {
|
||||
}
|
||||
|
||||
const toastBuilder = task.status === 'completed' ? toast.success : toast.error;
|
||||
const primaryAction = task.notification.actions.find((entry) => entry.primary && entry.id !== 'open_details')
|
||||
?? task.notification.actions.find((entry) => entry.id !== 'open_details')
|
||||
?? null;
|
||||
|
||||
toastBuilder(taskTitle(task), {
|
||||
id: task.id,
|
||||
duration: 10_000,
|
||||
description: taskDescription(task),
|
||||
description: terminalToastDescription(task),
|
||||
action: {
|
||||
label: 'Open details',
|
||||
onClick: () => openTaskDetails(task.id)
|
||||
label: primaryAction?.label ?? 'Open details',
|
||||
onClick: () => {
|
||||
if (primaryAction) {
|
||||
openTaskAction(task, primaryAction.id);
|
||||
return;
|
||||
}
|
||||
|
||||
openTaskDetails(task.id);
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
label: 'Mark read',
|
||||
@@ -223,7 +253,7 @@ export function useTaskNotificationsCenter(): UseTaskNotificationsCenterResult {
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [markTaskRead, openTaskDetails, silenceTask]);
|
||||
}, [markTaskRead, openTaskAction, openTaskDetails, silenceTask]);
|
||||
|
||||
const processSnapshots = useCallback(() => {
|
||||
const active = activeSnapshotRef.current;
|
||||
@@ -461,6 +491,7 @@ export function useTaskNotificationsCenter(): UseTaskNotificationsCenterResult {
|
||||
isDetailOpen,
|
||||
setIsDetailOpen,
|
||||
openTaskDetails,
|
||||
openTaskAction,
|
||||
markTaskRead,
|
||||
silenceTask,
|
||||
refreshTasks
|
||||
|
||||
Reference in New Issue
Block a user