diff --git a/web/src/app/home/agents/AgentDetailContent.tsx b/web/src/app/home/agents/AgentDetailContent.tsx index cfd75d3cb..95813ce8d 100644 --- a/web/src/app/home/agents/AgentDetailContent.tsx +++ b/web/src/app/home/agents/AgentDetailContent.tsx @@ -1,3 +1,4 @@ +import EntityLoadState from '@/components/EntityLoadState'; import { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -43,6 +44,8 @@ export default function AgentDetailContent({ id }: { id: string }) { const { refreshPipelines, pipelines, setDetailEntityName } = useSidebarData(); const [agent, setAgent] = useState(null); const [platformTools, setPlatformTools] = useState([]); + const [loadFailed, setLoadFailed] = useState(false); + const [loadAttempt, setLoadAttempt] = useState(0); const [loading, setLoading] = useState(!isCreateMode); const [formDirty, setFormDirty] = useState(false); const [formSaving, setFormSaving] = useState(false); @@ -77,6 +80,7 @@ export default function AgentDetailContent({ id }: { id: string }) { if (isCreateMode) return; let cancelled = false; setLoading(true); + setLoadFailed(false); Promise.all([ httpClient.getAgent(id), httpClient.getAdapters().catch(() => ({ adapters: [] })), @@ -97,13 +101,16 @@ export default function AgentDetailContent({ id }: { id: string }) { ); setAgent(resp.agent); }) + .catch(() => { + if (!cancelled) setLoadFailed(true); + }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; - }, [id, isCreateMode]); + }, [id, isCreateMode, loadAttempt]); if (isCreateMode) { return ( @@ -116,13 +123,11 @@ export default function AgentDetailContent({ id }: { id: string }) { ); } - if (loading || !agent) { + if (loadFailed) return ( -
- {t('common.loading')} -
+ setLoadAttempt((n) => n + 1)} /> ); - } + if (loading || !agent) return ; if (agent.kind === 'pipeline') { return ; diff --git a/web/src/app/home/agents/PluginProcessorDetailContent.tsx b/web/src/app/home/agents/PluginProcessorDetailContent.tsx index a2a9c5c08..9b0f4a6a4 100644 --- a/web/src/app/home/agents/PluginProcessorDetailContent.tsx +++ b/web/src/app/home/agents/PluginProcessorDetailContent.tsx @@ -1,3 +1,4 @@ +import EntityLoadState from '@/components/EntityLoadState'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link, useSearchParams } from 'react-router-dom'; @@ -79,6 +80,7 @@ export default function PluginProcessorDetailContent({ const [events, setEvents] = useState([]); const [eventCursor, setEventCursor] = useState(null); const [loading, setLoading] = useState(true); + const [initialLoadComplete, setInitialLoadComplete] = useState(false); const [saving, setSaving] = useState(false); const [pagingRuns, setPagingRuns] = useState(false); const [pagingEvents, setPagingEvents] = useState(false); @@ -89,6 +91,7 @@ export default function PluginProcessorDetailContent({ const available = Boolean(component); const load = useCallback(async () => { + setLoading(true); setFailed(false); try { const [metadata, page] = await Promise.all([ @@ -99,6 +102,7 @@ export default function PluginProcessorDetailContent({ setPlatformTools(metadata.platform_tools ?? []); setRuns(page.items); setCursor(page.has_more ? page.next_cursor : null); + setInitialLoadComplete(true); } catch { setFailed(true); } finally { @@ -385,6 +389,9 @@ export default function PluginProcessorDetailContent({ ); + if (!initialLoadComplete) + return void load()} />; + return ( - {t( - kind === 'event_processor' - ? 'agents.eventProcessor.create' - : 'common.submit', - )} + {t('common.submit')} diff --git a/web/src/app/home/agents/components/AgentFormComponent.tsx b/web/src/app/home/agents/components/AgentFormComponent.tsx index 3806e4a48..25e6bcb76 100644 --- a/web/src/app/home/agents/components/AgentFormComponent.tsx +++ b/web/src/app/home/agents/components/AgentFormComponent.tsx @@ -1,3 +1,4 @@ +import EntityLoadState from '@/components/EntityLoadState'; import { forwardRef, type ForwardedRef, @@ -145,6 +146,8 @@ function AgentFormComponent( useState(null); const [pluginStatusLoading, setPluginStatusLoading] = useState(true); const [pluginStatusError, setPluginStatusError] = useState(false); + const [loadFailed, setLoadFailed] = useState(false); + const [loadAttempt, setLoadAttempt] = useState(0); const [initialDataLoaded, setInitialDataLoaded] = useState(false); const [runnerInstallRecovering, setRunnerInstallRecovering] = useState(false); const [activeSection, setActiveSection] = @@ -208,6 +211,8 @@ function AgentFormComponent( useEffect(() => { let cancelled = false; + setInitialDataLoaded(false); + setLoadFailed(false); Promise.all([httpClient.getAgentMetadata(), httpClient.getAgent(agentId)]) .then(([metadata, resp]) => { if (cancelled) return; @@ -274,12 +279,14 @@ function AgentFormComponent( setInitialDataLoaded(true); }) .catch((err) => { + if (cancelled) return; + setLoadFailed(true); toast.error(t('agents.loadError') + err.msg); }); return () => { cancelled = true; }; - }, [agentId, form, t]); + }, [agentId, form, t, loadAttempt]); useEffect(() => { if (!initialDataLoaded || !readPendingRunnerInstall(runnerInstallScope)) { @@ -390,7 +397,8 @@ function AgentFormComponent( ]; const runnerStatus = useMemo(() => { - if (pluginStatusLoading) { + if (loadFailed) return { label: t('common.loadFailed'), tone: 'error' }; + if (!initialDataLoaded || pluginStatusLoading) { return { label: t('agents.runnerStatusLoading'), tone: 'neutral', @@ -459,6 +467,8 @@ function AgentFormComponent( tone: 'success', }; }, [ + initialDataLoaded, + loadFailed, currentRunner, pluginStatusError, pluginStatusLoading, @@ -635,6 +645,7 @@ function AgentFormComponent( } }, async save() { + if (!initialDataLoaded || loadFailed) return false; if (!hasUnsavedChangesRef.current) return true; if (isSavingRef.current) return false; const valid = await form.trigger(); @@ -642,9 +653,15 @@ function AgentFormComponent( return (await saveValues(form.getValues())) ?? false; }, }), - [form, saveValues], + [form, initialDataLoaded, loadFailed, saveValues], ); + if (loadFailed) + return ( + setLoadAttempt((n) => n + 1)} /> + ); + if (!initialDataLoaded) return ; + return (
diff --git a/web/src/app/home/agents/page.tsx b/web/src/app/home/agents/page.tsx index fb13ec41e..169717a56 100644 --- a/web/src/app/home/agents/page.tsx +++ b/web/src/app/home/agents/page.tsx @@ -8,7 +8,7 @@ export default function AgentsPage() { const detailId = searchParams.get('id'); if (detailId) { - return ; + return ; } return ( diff --git a/web/src/app/home/bots/BotDetailContent.tsx b/web/src/app/home/bots/BotDetailContent.tsx index a42a02b09..06bdc141c 100644 --- a/web/src/app/home/bots/BotDetailContent.tsx +++ b/web/src/app/home/bots/BotDetailContent.tsx @@ -1,3 +1,4 @@ +import EntityLoadState from '@/components/EntityLoadState'; import { useState, useEffect, useRef, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; @@ -59,6 +60,8 @@ export default function BotDetailContent({ id }: { id: string }) { const [adapterLabel, setAdapterLabel] = useState(''); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [basicInfoOpen, setBasicInfoOpen] = useState(false); + const [loadFailed, setLoadFailed] = useState(false); + const [loadAttempt, setLoadAttempt] = useState(0); const [bot, setBot] = useState(null); const [isRefreshingSessions, setIsRefreshingSessions] = useState(false); const sessionMonitorRef = useRef(null); @@ -74,13 +77,17 @@ export default function BotDetailContent({ id }: { id: string }) { // Fetch bot enable state useEffect(() => { if (!isCreateMode) { - httpClient.getBot(id).then((res) => { - setBot(res.bot); - setBotEnabled(res.bot.enable ?? true); - setEnableLoaded(true); - }); + setLoadFailed(false); + httpClient + .getBot(id) + .then((res) => { + setBot(res.bot); + setBotEnabled(res.bot.enable ?? true); + setEnableLoaded(true); + }) + .catch(() => setLoadFailed(true)); } - }, [id, isCreateMode]); + }, [id, isCreateMode, loadAttempt]); const handleEnableToggle = useCallback( async (checked: boolean) => { @@ -178,6 +185,12 @@ export default function BotDetailContent({ id }: { id: string }) { ); } + if (loadFailed) + return ( + setLoadAttempt((n) => n + 1)} /> + ); + if (!enableLoaded) return ; + // ==================== Edit Mode ==================== return ( <> diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx index 7bb36373a..a861ebc6e 100644 --- a/web/src/app/home/bots/components/bot-form/BotForm.tsx +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -1,3 +1,4 @@ +import EntityLoadState from '@/components/EntityLoadState'; import { showBotError } from '../../bot-error'; import React, { forwardRef, @@ -137,6 +138,9 @@ const BotForm = forwardRef(function BotForm( // Track whether initial data loading is complete. // setValue calls during init should NOT mark the form as dirty. + const [loadFailed, setLoadFailed] = useState(false); + const [loadAttempt, setLoadAttempt] = useState(0); + const [initialDataLoaded, setInitialDataLoaded] = useState(false); const isInitializing = useRef(true); const [adapterNameToDynamicConfigMap, setAdapterNameToDynamicConfigMap] = @@ -225,49 +229,55 @@ const BotForm = forwardRef(function BotForm( useEffect(() => { setBotFormValues(); - }, []); + }, [initBotId, loadAttempt]); function setBotFormValues() { + setInitialDataLoaded(false); + setLoadFailed(false); isInitializing.current = true; - initBotFormComponent().then(() => { - if (initBotId) { - getBotConfig(initBotId) - .then((val) => { - // Use form.reset() to set values AND update the dirty baseline, - // so isDirty stays false after initial load. - form.reset({ - name: val.name, - description: val.description, - adapter: val.adapter, - adapter_config: val.adapter_config, - enable: val.enable, - event_bindings: val.event_bindings || [], - plugin_processors: val.plugin_processors || [], - }); - handleAdapterSelect(val.adapter); + initBotFormComponent() + .then(() => { + if (initBotId) { + return getBotConfig(initBotId) + .then((val) => { + // Use form.reset() to set values AND update the dirty baseline, + // so isDirty stays false after initial load. + form.reset({ + name: val.name, + description: val.description, + adapter: val.adapter, + adapter_config: val.adapter_config, + enable: val.enable, + event_bindings: val.event_bindings || [], + plugin_processors: val.plugin_processors || [], + }); + handleAdapterSelect(val.adapter); - if (val.webhook_full_url) { - setWebhookUrl(val.webhook_full_url); - } else { - setWebhookUrl(''); - } - setExtraWebhookUrl(val.extra_webhook_full_url || ''); - }) - .catch((err) => { - toast.error( - t('bots.getBotConfigError') + (err as CustomApiError).msg, - ); - }) - .finally(() => { - isInitializing.current = false; - }); - } else { - form.reset(); - setWebhookUrl(''); - setExtraWebhookUrl(''); - isInitializing.current = false; - } - }); + if (val.webhook_full_url) { + setWebhookUrl(val.webhook_full_url); + } else { + setWebhookUrl(''); + } + setExtraWebhookUrl(val.extra_webhook_full_url || ''); + }) + .catch((err) => { + setLoadFailed(true); + toast.error( + t('bots.getBotConfigError') + (err as CustomApiError).msg, + ); + }) + .finally(() => { + isInitializing.current = false; + }); + } else { + form.reset(); + setWebhookUrl(''); + setExtraWebhookUrl(''); + isInitializing.current = false; + } + }) + .catch(() => setLoadFailed(true)) + .finally(() => setInitialDataLoaded(true)); } async function initBotFormComponent() { @@ -460,6 +470,12 @@ const BotForm = forwardRef(function BotForm( } } + if (loadFailed) + return ( + setLoadAttempt((n) => n + 1)} /> + ); + if (!initialDataLoaded) return ; + return ( ; + return ; } return ( diff --git a/web/src/app/home/knowledge/KBDetailContent.tsx b/web/src/app/home/knowledge/KBDetailContent.tsx index 4df7f1bfa..d138063c5 100644 --- a/web/src/app/home/knowledge/KBDetailContent.tsx +++ b/web/src/app/home/knowledge/KBDetailContent.tsx @@ -1,3 +1,4 @@ +import EntityLoadState from '@/components/EntityLoadState'; import { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; @@ -57,16 +58,20 @@ export default function KBDetailContent({ id }: { id: string }) { const [activeTab, setActiveTab] = useState('metadata'); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showBasicInfoDialog, setShowBasicInfoDialog] = useState(false); + const [loadFailed, setLoadFailed] = useState(false); + const [loadAttempt, setLoadAttempt] = useState(0); const [kbInfo, setKbInfo] = useState(null); const [formDirty, setFormDirty] = useState(false); const [formVersion, setFormVersion] = useState(0); const loadKbInfo = useCallback( async (kbId: string) => { + setLoadFailed(false); try { const resp = await httpClient.getKnowledgeBase(kbId); setKbInfo(resp.base); } catch (e) { + setLoadFailed(true); console.error('Failed to load KB info:', e); toast.error( t('knowledge.loadKnowledgeBaseFailed') + (e as CustomApiError).msg, @@ -81,7 +86,7 @@ export default function KBDetailContent({ id }: { id: string }) { if (!isCreateMode) { loadKbInfo(id); } - }, [id, isCreateMode, loadKbInfo]); + }, [id, isCreateMode, loadKbInfo, loadAttempt]); const hasDocumentCapability = (): boolean => { if (!kbInfo || !kbInfo.knowledge_engine) return false; @@ -179,6 +184,12 @@ export default function KBDetailContent({ id }: { id: string }) { ); } + if (loadFailed) + return ( + setLoadAttempt((n) => n + 1)} /> + ); + if (!kbInfo) return ; + // ==================== Edit Mode ==================== return ( <> diff --git a/web/src/app/home/knowledge/components/kb-form/KBForm.tsx b/web/src/app/home/knowledge/components/kb-form/KBForm.tsx index bd3f8d269..325822343 100644 --- a/web/src/app/home/knowledge/components/kb-form/KBForm.tsx +++ b/web/src/app/home/knowledge/components/kb-form/KBForm.tsx @@ -1,3 +1,4 @@ +import EntityLoadState from '@/components/EntityLoadState'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -94,6 +95,9 @@ export default function KBForm({ Record >({}); const [isEditing, setIsEditing] = useState(Boolean(initKbId)); + const [loadFailed, setLoadFailed] = useState(false); + const [loadAttempt, setLoadAttempt] = useState(0); + const [initialDataLoaded, setInitialDataLoaded] = useState(false); const [loading, setLoading] = useState(true); // Dirty tracking: snapshot of saved state for comparison @@ -144,12 +148,15 @@ export default function KBForm({ }; useEffect(() => { - loadRagEngines().then(() => { - if (initKbId) { - loadKbConfig(initKbId); - } - }); - }, []); + setInitialDataLoaded(false); + setLoadFailed(false); + loadRagEngines() + .then(() => { + if (initKbId) return loadKbConfig(initKbId); + }) + .catch(() => setLoadFailed(true)) + .finally(() => setInitialDataLoaded(true)); + }, [initKbId, loadAttempt]); // Auto-select first engine when engines are loaded and no selection useEffect(() => { @@ -178,7 +185,7 @@ export default function KBForm({ const resp = await httpClient.getKnowledgeEngines(); setRagEngines(resp.engines); } catch (err) { - console.error('Failed to load Knowledge Engines:', err); + throw err; } finally { setLoading(false); } @@ -211,8 +218,8 @@ export default function KBForm({ isInitializing.current = false; }, 500); } catch (err) { - console.error('Failed to load KB config:', err); isInitializing.current = false; + throw err; } }; @@ -321,6 +328,12 @@ export default function KBForm({ [selectedEngine?.retrieval_schema], ); + if (loadFailed) + return ( + setLoadAttempt((n) => n + 1)} /> + ); + if (!initialDataLoaded) return ; + return ( - + ); } diff --git a/web/src/app/home/mcp/MCPDetailContent.tsx b/web/src/app/home/mcp/MCPDetailContent.tsx index f0e853e8f..9403febf2 100644 --- a/web/src/app/home/mcp/MCPDetailContent.tsx +++ b/web/src/app/home/mcp/MCPDetailContent.tsx @@ -1,3 +1,4 @@ +import EntityLoadState from '@/components/EntityLoadState'; import { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button } from '@/components/ui/button'; @@ -74,6 +75,8 @@ export default function MCPDetailContent({ id }: { id: string }) { // Enable state managed here so the header switch works const [serverEnabled, setServerEnabled] = useState(true); + const [loadFailed, setLoadFailed] = useState(false); + const [loadAttempt, setLoadAttempt] = useState(0); const [enableLoaded, setEnableLoaded] = useState(false); const [detailRuntimeStatus, setDetailRuntimeStatus] = useState(null); @@ -120,14 +123,18 @@ export default function MCPDetailContent({ id }: { id: string }) { useEffect(() => { if (!isCreateMode) { setDetailRuntimeStatus(null); - httpClient.getMCPServer(id).then((res) => { - const server = res.server ?? res; - setServerEnabled(server.enable ?? true); - setDetailRuntimeStatus(server.runtime_info?.status ?? null); - setEnableLoaded(true); - }); + setLoadFailed(false); + httpClient + .getMCPServer(id) + .then((res) => { + const server = res.server ?? res; + setServerEnabled(server.enable ?? true); + setDetailRuntimeStatus(server.runtime_info?.status ?? null); + setEnableLoaded(true); + }) + .catch(() => setLoadFailed(true)); } - }, [id, isCreateMode]); + }, [id, isCreateMode, loadAttempt]); const handleEnableToggle = useCallback( async (checked: boolean) => { @@ -325,6 +332,12 @@ export default function MCPDetailContent({ id }: { id: string }) { ); // ==================== Edit Mode ==================== + if (loadFailed) + return ( + setLoadAttempt((n) => n + 1)} /> + ); + if (!enableLoaded) return ; + return ( <>
diff --git a/web/src/app/home/mcp/components/mcp-form/MCPForm.tsx b/web/src/app/home/mcp/components/mcp-form/MCPForm.tsx index d1e4140a4..84df28987 100644 --- a/web/src/app/home/mcp/components/mcp-form/MCPForm.tsx +++ b/web/src/app/home/mcp/components/mcp-form/MCPForm.tsx @@ -1,3 +1,4 @@ +import EntityLoadState from '@/components/EntityLoadState'; import React, { type ReactNode, useState, @@ -563,6 +564,7 @@ const MCPForm = forwardRef(function MCPForm( const pollingIntervalRef = useRef(null); const watchMode = form.watch('mode'); const { + loading: boxLoading, available: boxAvailable, hint: boxHint, reason: boxReason, @@ -577,6 +579,9 @@ const MCPForm = forwardRef(function MCPForm( watchMode === 'stdio' && mcpStdioEnabled && !boxAvailable; const stdioBlocked = stdioBlockedByPolicy || stdioBlockedByBox; + const [loadFailed, setLoadFailed] = useState(false); + const [loadAttempt, setLoadAttempt] = useState(0); + const [initialDataLoaded, setInitialDataLoaded] = useState(!isEditMode); const { isDirty } = form.formState; useEffect(() => { onDirtyChange?.(isDirty); @@ -606,10 +611,13 @@ const MCPForm = forwardRef(function MCPForm( ); useEffect(() => { + setLoadFailed(false); + setInitialDataLoaded(!isEditMode); isInitializing.current = true; if (isEditMode && initServerName) { loadServerForEdit(initServerName).finally(() => { isInitializing.current = false; + setInitialDataLoaded(true); }); } else { form.reset({ @@ -636,7 +644,7 @@ const MCPForm = forwardRef(function MCPForm( pollingIntervalRef.current = null; } }; - }, [initServerName]); + }, [initServerName, loadAttempt]); useEffect(() => { if (!onDraftChange || isEditMode) return; @@ -756,6 +764,7 @@ const MCPForm = forwardRef(function MCPForm( setRuntimeInfo(server.runtime_info ?? null); setReadme(server.readme ?? ''); } catch (error) { + setLoadFailed(true); console.error('Failed to load server:', error); toast.error(t('mcp.loadFailed')); } @@ -1337,6 +1346,12 @@ const MCPForm = forwardRef(function MCPForm( runtimePanel ); + if (loadFailed) + return ( + setLoadAttempt((n) => n + 1)} /> + ); + if (!initialDataLoaded || boxLoading) return ; + if (layout === 'split') { return ( diff --git a/web/src/app/home/mcp/page.tsx b/web/src/app/home/mcp/page.tsx index 391679c37..387c748c2 100644 --- a/web/src/app/home/mcp/page.tsx +++ b/web/src/app/home/mcp/page.tsx @@ -8,7 +8,7 @@ export default function MCPPage() { const detailId = searchParams.get('id'); if (detailId) { - return ; + return ; } return ( diff --git a/web/src/app/home/pipelines/PipelineDetailContent.tsx b/web/src/app/home/pipelines/PipelineDetailContent.tsx index e2c98851b..a302db6b6 100644 --- a/web/src/app/home/pipelines/PipelineDetailContent.tsx +++ b/web/src/app/home/pipelines/PipelineDetailContent.tsx @@ -1,3 +1,4 @@ +import EntityLoadState from '@/components/EntityLoadState'; import { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; @@ -52,6 +53,8 @@ export default function PipelineDetailContent({ const [formDirty, setFormDirty] = useState(false); const [formSaving, setFormSaving] = useState(false); const [basicInfoOpen, setBasicInfoOpen] = useState(false); + const [loadFailed, setLoadFailed] = useState(false); + const [loadAttempt, setLoadAttempt] = useState(0); const [pipelineDetails, setPipelineDetails] = useState(null); const pipelineFormRef = useRef(null); const sidebarPipeline = pipelines.find((item) => item.id === id); @@ -59,13 +62,17 @@ export default function PipelineDetailContent({ useEffect(() => { if (isCreateMode) return; let cancelled = false; - httpClient.getPipeline(id).then((response) => { - if (!cancelled) setPipelineDetails(response.pipeline); - }); + setLoadFailed(false); + httpClient + .getPipeline(id) + .then((response) => { + if (!cancelled) setPipelineDetails(response.pipeline); + }) + .catch(() => setLoadFailed(true)); return () => { cancelled = true; }; - }, [id, isCreateMode]); + }, [id, isCreateMode, loadAttempt]); function handleFinish() { refreshPipelines(); @@ -137,6 +144,12 @@ export default function PipelineDetailContent({ navigate(routeBase); } + if (loadFailed) + return ( + setLoadAttempt((n) => n + 1)} /> + ); + if (!pipelineDetails) return ; + // ==================== Edit Mode ==================== const pipelineName = pipelineDetails?.name || diff --git a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx index 253c53d9a..b9f72add2 100644 --- a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx +++ b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx @@ -1,3 +1,4 @@ +import EntityLoadState from '@/components/EntityLoadState'; import { forwardRef, useCallback, @@ -211,6 +212,8 @@ const PipelineFormComponent = forwardRef< useState(); const [outputConfigTabSchema, setOutputConfigTabSchema] = useState(); + const [loadFailed, setLoadFailed] = useState(false); + const [loadAttempt, setLoadAttempt] = useState(0); const [metadataLoaded, setMetadataLoaded] = useState(false); const [pipelineLoaded, setPipelineLoaded] = useState(!isEditMode); @@ -257,28 +260,37 @@ const PipelineFormComponent = forwardRef< }, [hasUnsavedChanges, onDirtyChange]); useEffect(() => { + let cancelled = false; + setLoadFailed(false); setMetadataLoaded(false); setPipelineLoaded(!isEditMode); // get config schema from metadata - httpClient.getGeneralPipelineMetadata().then((resp) => { - for (const config of resp.configs) { - if (config.name === 'ai') { - setAIConfigTabSchema(config); - } else if (config.name === 'trigger') { - setTriggerConfigTabSchema(config); - } else if (config.name === 'safety') { - setSafetyConfigTabSchema(config); - } else if (config.name === 'output') { - setOutputConfigTabSchema(config); + httpClient + .getGeneralPipelineMetadata() + .then((resp) => { + if (cancelled) return; + for (const config of resp.configs) { + if (config.name === 'ai') { + setAIConfigTabSchema(config); + } else if (config.name === 'trigger') { + setTriggerConfigTabSchema(config); + } else if (config.name === 'safety') { + setSafetyConfigTabSchema(config); + } else if (config.name === 'output') { + setOutputConfigTabSchema(config); + } } - } - setMetadataLoaded(true); - }); + setMetadataLoaded(true); + }) + .catch(() => { + if (!cancelled) setLoadFailed(true); + }); if (isEditMode) { httpClient .getPipeline(pipelineId || '') .then((resp: GetPipelineResponseData) => { + if (cancelled) return; setIsDefaultPipeline(resp.pipeline.is_default ?? false); const loadedValues = { @@ -296,9 +308,15 @@ const PipelineFormComponent = forwardRef< savedSnapshotRef.current = JSON.stringify(loadedValues); initializedStagesRef.current.clear(); setPipelineLoaded(true); + }) + .catch(() => { + if (!cancelled) setLoadFailed(true); }); } - }, [form, isEditMode, pipelineId]); + return () => { + cancelled = true; + }; + }, [form, isEditMode, pipelineId, loadAttempt]); useEffect(() => { if ( @@ -693,6 +711,12 @@ const PipelineFormComponent = forwardRef< } }; + if (loadFailed) + return ( + setLoadAttempt((n) => n + 1)} /> + ); + if (!metadataLoaded || !pipelineLoaded) return ; + return ( <>
diff --git a/web/src/app/home/pipelines/page.tsx b/web/src/app/home/pipelines/page.tsx index be65346a9..e4d155ccf 100644 --- a/web/src/app/home/pipelines/page.tsx +++ b/web/src/app/home/pipelines/page.tsx @@ -8,7 +8,7 @@ export default function PipelineConfigPage() { const detailId = searchParams.get('id'); if (detailId) { - return ; + return ; } return ( diff --git a/web/src/app/home/plugin-pages/page.tsx b/web/src/app/home/plugin-pages/page.tsx index a51df38ec..520240791 100644 --- a/web/src/app/home/plugin-pages/page.tsx +++ b/web/src/app/home/plugin-pages/page.tsx @@ -1,3 +1,4 @@ +import EntityLoadState from '@/components/EntityLoadState'; import { useSearchParams } from 'react-router-dom'; import { httpClient } from '@/app/infra/http/HttpClient'; import { useEffect, useRef, useState, useCallback } from 'react'; @@ -78,11 +79,7 @@ export default function PluginPagesPage() {
); } - return ( -
- Loading... -
- ); + return ; } const assetPath = page.path; @@ -209,9 +206,7 @@ function PluginPageIframe({ {t('plugins.loadFailed')}
) : loading || !assetUrl ? ( -
- Loading... -
+ ) : null} {!assetError && assetUrl && (