feat: implement error handling for task preview generation and add error message utility
All checks were successful
Build And Publish Production Image / Build And Publish Production Image (push) Successful in 14s
All checks were successful
Build And Publish Production Image / Build And Publish Production Image (push) Successful in 14s
This commit is contained in:
@@ -328,4 +328,18 @@ INSTRUCTIONS
|
|||||||
'backend unavailable'
|
'backend unavailable'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should_returnReadable401Message_when_generateTaskPreviewUnauthorized', async () => {
|
||||||
|
mockedApiClient.post.mockRejectedValue({
|
||||||
|
isAxiosError: true,
|
||||||
|
response: {
|
||||||
|
status: 401,
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(generateTaskPreview('task-1', { entity, task: previewTask })).rejects.toThrow(
|
||||||
|
'Your session has expired. Please sign in again and retry.'
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -205,7 +205,7 @@ describe('EditTaskPage', () => {
|
|||||||
|
|
||||||
it('should_showReadablePreviewError_when_generationFails', async () => {
|
it('should_showReadablePreviewError_when_generationFails', async () => {
|
||||||
vi.mocked(tasksApi.generateTaskPreview).mockRejectedValue(
|
vi.mocked(tasksApi.generateTaskPreview).mockRejectedValue(
|
||||||
new Error('Unable to generate a test message from the local model. Connection refused')
|
new Error('Your session has expired. Please sign in again and retry.')
|
||||||
)
|
)
|
||||||
|
|
||||||
renderPage()
|
renderPage()
|
||||||
@@ -215,7 +215,7 @@ describe('EditTaskPage', () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(/Unable to generate a test message from the local model. Connection refused/i)
|
screen.getByText(/Your session has expired. Please sign in again and retry./i)
|
||||||
).toBeInTheDocument()
|
).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
53
frontend/src/api/errorMessage.ts
Normal file
53
frontend/src/api/errorMessage.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { isAxiosError } from 'axios'
|
||||||
|
|
||||||
|
function getMessageFromResponseData(data: unknown): string | null {
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = data as Record<string, unknown>
|
||||||
|
const candidate = payload.message ?? payload.error ?? payload.detail
|
||||||
|
|
||||||
|
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
||||||
|
return candidate.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getApiErrorMessage(error: unknown, fallback: string): string {
|
||||||
|
if (isAxiosError(error)) {
|
||||||
|
const status = error.response?.status
|
||||||
|
const responseMessage = getMessageFromResponseData(error.response?.data)
|
||||||
|
|
||||||
|
if (responseMessage) {
|
||||||
|
return responseMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 401) {
|
||||||
|
return 'Your session has expired. Please sign in again and retry.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 403) {
|
||||||
|
return 'You do not have permission to perform this action.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 404) {
|
||||||
|
return 'The requested resource was not found.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status && status >= 500) {
|
||||||
|
return 'The server failed to process the request. Please try again in a moment.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!error.response) {
|
||||||
|
return 'Unable to reach the server. Check your network connection and try again.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error && error.message.trim().length > 0) {
|
||||||
|
return error.message
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { VirtualEntityResponse } from './entitiesApi'
|
import type { VirtualEntityResponse } from './entitiesApi'
|
||||||
import apiClient from './apiClient'
|
import apiClient from './apiClient'
|
||||||
|
import { getApiErrorMessage } from './errorMessage'
|
||||||
|
|
||||||
export type EmailLookback = 'last_day' | 'last_week' | 'last_month'
|
export type EmailLookback = 'last_day' | 'last_week' | 'last_month'
|
||||||
export type GenerationSource = 'openai' | 'llama'
|
export type GenerationSource = 'openai' | 'llama'
|
||||||
@@ -88,11 +89,17 @@ export async function generateTaskPreview(
|
|||||||
taskId: string,
|
taskId: string,
|
||||||
payload: TaskPreviewRequest
|
payload: TaskPreviewRequest
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const response = await apiClient.post<GeneratedMessageHistoryItem>(
|
try {
|
||||||
`/v1/tasks/${taskId}/generated-messages/generate`,
|
const response = await apiClient.post<GeneratedMessageHistoryItem>(
|
||||||
payload
|
`/v1/tasks/${taskId}/generated-messages/generate`,
|
||||||
)
|
payload
|
||||||
return response.data.content
|
)
|
||||||
|
return response.data.content
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
getApiErrorMessage(error, 'Unable to generate a test message right now. Please try again.')
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns generated message history for one task. */
|
/** Returns generated message history for one task. */
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||||
|
import { getApiErrorMessage } from '../api/errorMessage'
|
||||||
import { getEntity } from '../api/entitiesApi'
|
import { getEntity } from '../api/entitiesApi'
|
||||||
import {
|
import {
|
||||||
activateTask,
|
activateTask,
|
||||||
@@ -122,6 +123,7 @@ export default function EditTaskPage() {
|
|||||||
const [taskForm, setTaskForm] = useState<TaskFormState>(DEFAULT_TASK_FORM)
|
const [taskForm, setTaskForm] = useState<TaskFormState>(DEFAULT_TASK_FORM)
|
||||||
const [selectedMessageId, setSelectedMessageId] = useState('')
|
const [selectedMessageId, setSelectedMessageId] = useState('')
|
||||||
const [previewError, setPreviewError] = useState('')
|
const [previewError, setPreviewError] = useState('')
|
||||||
|
const [actionError, setActionError] = useState('')
|
||||||
|
|
||||||
const { data: entity, isLoading: isLoadingEntity } = useQuery({
|
const { data: entity, isLoading: isLoadingEntity } = useQuery({
|
||||||
queryKey: ['entity', entityId],
|
queryKey: ['entity', entityId],
|
||||||
@@ -181,34 +183,58 @@ export default function EditTaskPage() {
|
|||||||
emailLookback: data.emailLookback,
|
emailLookback: data.emailLookback,
|
||||||
generationSource: data.generationSource,
|
generationSource: data.generationSource,
|
||||||
}),
|
}),
|
||||||
|
onMutate: () => {
|
||||||
|
setActionError('')
|
||||||
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await invalidateTaskQueries(queryClient, entityId, taskId)
|
await invalidateTaskQueries(queryClient, entityId, taskId)
|
||||||
navigate(`/entities/${entityId}`)
|
navigate(`/entities/${entityId}`)
|
||||||
},
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setActionError(getApiErrorMessage(error, 'Unable to save the task right now. Please try again.'))
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const inactivateTaskMutation = useMutation({
|
const inactivateTaskMutation = useMutation({
|
||||||
mutationFn: () => inactivateTask(taskId),
|
mutationFn: () => inactivateTask(taskId),
|
||||||
|
onMutate: () => {
|
||||||
|
setActionError('')
|
||||||
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await invalidateTaskQueries(queryClient, entityId, taskId)
|
await invalidateTaskQueries(queryClient, entityId, taskId)
|
||||||
navigate(`/entities/${entityId}`)
|
navigate(`/entities/${entityId}`)
|
||||||
},
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setActionError(getApiErrorMessage(error, 'Unable to inactivate the task right now. Please try again.'))
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const activateTaskMutation = useMutation({
|
const activateTaskMutation = useMutation({
|
||||||
mutationFn: () => activateTask(taskId),
|
mutationFn: () => activateTask(taskId),
|
||||||
|
onMutate: () => {
|
||||||
|
setActionError('')
|
||||||
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await invalidateTaskQueries(queryClient, entityId, taskId)
|
await invalidateTaskQueries(queryClient, entityId, taskId)
|
||||||
navigate(`/entities/${entityId}`)
|
navigate(`/entities/${entityId}`)
|
||||||
},
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setActionError(getApiErrorMessage(error, 'Unable to activate the task right now. Please try again.'))
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteTaskMutation = useMutation({
|
const deleteTaskMutation = useMutation({
|
||||||
mutationFn: () => deleteTask(taskId),
|
mutationFn: () => deleteTask(taskId),
|
||||||
|
onMutate: () => {
|
||||||
|
setActionError('')
|
||||||
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await invalidateTaskQueries(queryClient, entityId, taskId)
|
await invalidateTaskQueries(queryClient, entityId, taskId)
|
||||||
navigate(`/entities/${entityId}`)
|
navigate(`/entities/${entityId}`)
|
||||||
},
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setActionError(getApiErrorMessage(error, 'Unable to delete the task right now. Please try again.'))
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const previewMutation = useMutation({
|
const previewMutation = useMutation({
|
||||||
@@ -222,9 +248,10 @@ export default function EditTaskPage() {
|
|||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
setPreviewError(
|
setPreviewError(
|
||||||
error instanceof Error
|
getApiErrorMessage(
|
||||||
? error.message
|
error,
|
||||||
: 'Unable to generate a test message from the local model.'
|
'Unable to generate a test message right now. Please try again.'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -268,6 +295,9 @@ export default function EditTaskPage() {
|
|||||||
|
|
||||||
const deleteGeneratedMessageMutation = useMutation({
|
const deleteGeneratedMessageMutation = useMutation({
|
||||||
mutationFn: (messageId: string) => deleteTaskGeneratedMessage(taskId, messageId),
|
mutationFn: (messageId: string) => deleteTaskGeneratedMessage(taskId, messageId),
|
||||||
|
onMutate: () => {
|
||||||
|
setActionError('')
|
||||||
|
},
|
||||||
onSuccess: ( _data, messageId) => {
|
onSuccess: ( _data, messageId) => {
|
||||||
queryClient.setQueryData(
|
queryClient.setQueryData(
|
||||||
['task-generated-messages', taskId],
|
['task-generated-messages', taskId],
|
||||||
@@ -278,6 +308,14 @@ export default function EditTaskPage() {
|
|||||||
) => currentMessages?.filter((message) => message.id !== messageId) ?? []
|
) => currentMessages?.filter((message) => message.id !== messageId) ?? []
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setActionError(
|
||||||
|
getApiErrorMessage(
|
||||||
|
error,
|
||||||
|
'Unable to delete the generated message right now. Please try again.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const applyCronParts = (nextCronParts: CronParts) => {
|
const applyCronParts = (nextCronParts: CronParts) => {
|
||||||
@@ -589,6 +627,12 @@ export default function EditTaskPage() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{actionError && (
|
||||||
|
<p role="alert" className="text-sm text-red-300">
|
||||||
|
{actionError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 pb-8">
|
<div className="flex justify-end gap-3 pb-8">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user