diff --git a/frontend/src/components/shared/data-load-error.tsx b/frontend/src/components/shared/data-load-error.tsx new file mode 100644 index 00000000..3582ff17 --- /dev/null +++ b/frontend/src/components/shared/data-load-error.tsx @@ -0,0 +1,18 @@ +import { AlertCircle } from 'lucide-react'; + +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; + +interface DataLoadErrorProps { + message?: string; + title: string; +} + +export function DataLoadError({ message, title }: DataLoadErrorProps) { + return ( + + + {title} + {message ? {message} : null} + + ); +} diff --git a/frontend/src/pages/dashboard/dashboard-analytics.tsx b/frontend/src/pages/dashboard/dashboard-analytics.tsx index 56ea9762..402e6bf4 100644 --- a/frontend/src/pages/dashboard/dashboard-analytics.tsx +++ b/frontend/src/pages/dashboard/dashboard-analytics.tsx @@ -8,6 +8,7 @@ import type { FlowFragmentFragment, UsageStatsPeriod } from '@/graphql/types'; import { ChartCard, ChartTooltip } from '@/components/dashboard'; import { FlowStatusBadge } from '@/components/icons/flow-status-badge'; +import { DataLoadError } from '@/components/shared/data-load-error'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; @@ -61,22 +62,38 @@ type FlowExecution = { }; export function DashboardAnalytics({ period }: { period: UsageStatsPeriod }) { - const { data: usageByPeriodData, loading: usageByPeriodLoading } = useQuery(UsageStatsByPeriodDocument, { + const { + data: usageByPeriodData, + error: usageByPeriodError, + loading: usageByPeriodLoading, + } = useQuery(UsageStatsByPeriodDocument, { variables: { period }, }); - const { data: toolcallsByPeriodData, loading: toolcallsByPeriodLoading } = useQuery( - ToolcallsStatsByPeriodDocument, - { - variables: { period }, - }, - ); - const { data: flowsByPeriodData, loading: flowsByPeriodLoading } = useQuery(FlowsStatsByPeriodDocument, { + const { + data: toolcallsByPeriodData, + error: toolcallsByPeriodError, + loading: toolcallsByPeriodLoading, + } = useQuery(ToolcallsStatsByPeriodDocument, { variables: { period }, }); - const { data: executionStatsData, loading: executionStatsLoading } = useQuery(FlowsExecutionStatsByPeriodDocument, { + const { + data: flowsByPeriodData, + error: flowsByPeriodError, + loading: flowsByPeriodLoading, + } = useQuery(FlowsStatsByPeriodDocument, { variables: { period }, }); - const { data: flowsData } = useQuery(FlowsDocument); + const { + data: executionStatsData, + error: executionStatsError, + loading: executionStatsLoading, + } = useQuery(FlowsExecutionStatsByPeriodDocument, { + variables: { period }, + }); + const { data: flowsData, error: flowsError } = useQuery(FlowsDocument); + + const loadError = + usageByPeriodError ?? toolcallsByPeriodError ?? flowsByPeriodError ?? executionStatsError ?? flowsError; const flowsTooltip = useChartTooltipAnimation(); const toolcallsTooltip = useChartTooltipAnimation(); @@ -137,6 +154,12 @@ export function DashboardAnalytics({ period }: { period: UsageStatsPeriod }) { return (
+ {loadError ? ( + + ) : null} + {loadError ? ( + + ) : null}
{ function Knowledges() { const navigate = useNavigate(); const location = useLocation(); - const { deleteKnowledge, isLoading, knowledges, renameKnowledge } = useKnowledges(); + const { deleteKnowledge, error, isLoading, knowledges, renameKnowledge } = useKnowledges(); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [deletingKnowledge, setDeletingKnowledge] = useState(null); const [deletingIds, setDeletingIds] = useState>(new Set()); @@ -422,6 +423,20 @@ function Knowledges() { ); } + if (error) { + return ( + <> + {pageHeader} +
+ +
+ + ); + } + if (!knowledges.length) { return ( <> diff --git a/frontend/src/pages/templates/templates.tsx b/frontend/src/pages/templates/templates.tsx index 8ba09f93..8d96bf2a 100644 --- a/frontend/src/pages/templates/templates.tsx +++ b/frontend/src/pages/templates/templates.tsx @@ -13,6 +13,7 @@ import { AppHeaderTitle, } from '@/components/layouts/app/app-header'; import ConfirmationDialog from '@/components/shared/confirmation-dialog'; +import { DataLoadError } from '@/components/shared/data-load-error'; import { InlineEditInput } from '@/components/shared/inline-edit'; import { Button } from '@/components/ui/button'; import { ContextMenuItem, ContextMenuSeparator } from '@/components/ui/context-menu'; @@ -34,7 +35,7 @@ import { type Template, useTemplates } from '@/providers/templates-provider'; function Templates() { const navigate = useNavigate(); const location = useLocation(); - const { deleteTemplate, templates, updateTemplate } = useTemplates(); + const { deleteTemplate, error, isLoading, templates, updateTemplate } = useTemplates(); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [deletingTemplate, setDeletingTemplate] = useState(null); const [deletingIds, setDeletingIds] = useState>(new Set()); @@ -264,6 +265,42 @@ function Templates() { ); + if (isLoading && !templates.length) { + return ( + <> + {pageHeader} +
+ + + + + + Loading templates... + Please wait while we fetch your flow templates + + +
+ + ); + } + + if (error) { + return ( + <> + {pageHeader} +
+ +
+ + ); + } + if (!templates.length) { return ( <> diff --git a/frontend/src/providers/knowledges-provider.tsx b/frontend/src/providers/knowledges-provider.tsx index 88ecf8a1..fd75606a 100644 --- a/frontend/src/providers/knowledges-provider.tsx +++ b/frontend/src/providers/knowledges-provider.tsx @@ -31,6 +31,7 @@ export type Knowledge = KnowledgeDocumentFragmentFragment; interface KnowledgesContextValue { createKnowledge: (input: CreateKnowledgeDocumentInput) => Promise; deleteKnowledge: (id: string) => Promise; + error?: Error; getKnowledge: (id: string) => Knowledge | undefined; isLoading: boolean; knowledges: Knowledge[]; @@ -71,7 +72,11 @@ export function KnowledgesProvider({ children }: KnowledgesProviderProps) { // That keeps Apollo's cache warm for the inactive branch — when the user // toggles `?qs=` on/off, the previous result is shown immediately while // the network refetches in the background. - const { data: listData, loading: isListLoading } = useQuery(KnowledgeDocumentsDocument, { + const { + data: listData, + error: listError, + loading: isListLoading, + } = useQuery(KnowledgeDocumentsDocument, { fetchPolicy: 'cache-and-network', nextFetchPolicy: 'cache-and-network', skip: !shouldFetch || inSearchMode, @@ -80,7 +85,11 @@ export function KnowledgesProvider({ children }: KnowledgesProviderProps) { // `searchKnowledge` ignores `withContent` — the backend always returns the full // chunk text plus a relevance score we currently drop. - const { data: searchData, loading: isSearchLoading } = useQuery(SearchKnowledgeDocument, { + const { + data: searchData, + error: searchError, + loading: isSearchLoading, + } = useQuery(SearchKnowledgeDocument, { fetchPolicy: 'cache-and-network', nextFetchPolicy: 'cache-and-network', skip: !shouldFetch || !inSearchMode, @@ -126,6 +135,7 @@ export function KnowledgesProvider({ children }: KnowledgesProviderProps) { }, [inSearchMode, listData?.knowledgeDocuments, searchData?.searchKnowledge]); const isLoading = inSearchMode ? isSearchLoading : isListLoading; + const error = inSearchMode ? searchError : listError; const getKnowledge = useCallback( (id: string): Knowledge | undefined => knowledges.find((k) => k.id === id), @@ -198,13 +208,23 @@ export function KnowledgesProvider({ children }: KnowledgesProviderProps) { () => ({ createKnowledge, deleteKnowledge, + error, getKnowledge, isLoading, knowledges, renameKnowledge, updateKnowledge, }), - [createKnowledge, deleteKnowledge, getKnowledge, isLoading, knowledges, renameKnowledge, updateKnowledge], + [ + createKnowledge, + deleteKnowledge, + error, + getKnowledge, + isLoading, + knowledges, + renameKnowledge, + updateKnowledge, + ], ); return {children}; diff --git a/frontend/src/providers/templates-provider.tsx b/frontend/src/providers/templates-provider.tsx index 83e8535b..5ad044de 100644 --- a/frontend/src/providers/templates-provider.tsx +++ b/frontend/src/providers/templates-provider.tsx @@ -26,6 +26,7 @@ export interface Template { interface TemplatesContextValue { createTemplate: (title: string, text: string) => Promise; deleteTemplate: (id: string) => Promise; + error?: Error; getTemplate: (id: string) => Template | undefined; isLoading: boolean; templates: Template[]; @@ -43,10 +44,11 @@ export function TemplatesProvider({ children }: TemplatesProviderProps) { const shouldFetchTemplates = Boolean(authInfo && authInfo.type !== 'guest' && isAuthenticated()); - const { data: templatesData, loading: isLoadingTemplates } = useQuery( - FlowTemplatesDocument, - shouldFetchTemplates ? { fetchPolicy: 'cache-and-network' } : skipToken, - ); + const { + data: templatesData, + error: templatesError, + loading: isLoadingTemplates, + } = useQuery(FlowTemplatesDocument, shouldFetchTemplates ? { fetchPolicy: 'cache-and-network' } : skipToken); const [createTemplateMutation] = useMutation(CreateFlowTemplateDocument); const [updateTemplateMutation] = useMutation(UpdateFlowTemplateDocument); @@ -155,12 +157,13 @@ export function TemplatesProvider({ children }: TemplatesProviderProps) { () => ({ createTemplate, deleteTemplate, + error: templatesError, getTemplate, isLoading: isLoadingTemplates, templates, updateTemplate, }), - [createTemplate, deleteTemplate, getTemplate, isLoadingTemplates, templates, updateTemplate], + [createTemplate, deleteTemplate, templatesError, getTemplate, isLoadingTemplates, templates, updateTemplate], ); return {children};