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