diff --git a/src/main/frontend/app/components/load-error.tsx b/src/main/frontend/app/components/load-error.tsx new file mode 100644 index 00000000..53ad5ef2 --- /dev/null +++ b/src/main/frontend/app/components/load-error.tsx @@ -0,0 +1,21 @@ +import clsx from 'clsx' +import Button from '~/components/inputs/button' + +type LoadErrorProperties = { + message?: string + onRetry: () => void + className?: string +} + +export default function LoadError({ + message = 'Something went wrong while loading.', + onRetry, + className, +}: Readonly) { + return ( +
+

{message}

+ +
+ ) +} diff --git a/src/main/frontend/app/providers/frankconfig-xsd-provider.tsx b/src/main/frontend/app/providers/frankconfig-xsd-provider.tsx index 41058fd5..0fb6be37 100644 --- a/src/main/frontend/app/providers/frankconfig-xsd-provider.tsx +++ b/src/main/frontend/app/providers/frankconfig-xsd-provider.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react' +import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react' import { fetchFrankConfigXsd } from '~/services/xsd-service' import { parseXsd } from '~/utils/xsd-utils' import { logApiWarning } from '~/utils/logger' @@ -6,22 +6,37 @@ import { logApiWarning } from '~/utils/logger' type FrankConfigXsdContextValue = { xsdContent: string | null xsdDoc: Document | null + error: Error | null + refetch: () => void } const FrankConfigXsdContext = createContext(null) export function FrankConfigXsdProvider({ children }: { children: ReactNode }) { const [xsdContent, setXsdContent] = useState(null) + const [error, setError] = useState(null) - useEffect(() => { + const load = useCallback(() => { + setError(null) fetchFrankConfigXsd() .then(setXsdContent) - .catch((error) => logApiWarning('Failed to load FrankConfig XSD:', error as Error)) + .catch((error_) => { + setError(error_ as Error) + logApiWarning('Failed to load FrankConfig XSD:', error_ as Error) + }) }, []) + useEffect(() => { + load() + }, [load]) + const xsdDoc = useMemo(() => (xsdContent ? parseXsd(xsdContent) : null), [xsdContent]) - return {children} + return ( + + {children} + + ) } export function useFrankConfigXsd(): FrankConfigXsdContextValue { diff --git a/src/main/frontend/app/routes/studio/context/studio-context.tsx b/src/main/frontend/app/routes/studio/context/studio-context.tsx index 0906c7c8..75fa564c 100644 --- a/src/main/frontend/app/routes/studio/context/studio-context.tsx +++ b/src/main/frontend/app/routes/studio/context/studio-context.tsx @@ -1,23 +1,25 @@ import useNodeContextStore from '~/stores/node-context-store' -import { useState, useMemo } from 'react' +import { useEffect, useState, useMemo } from 'react' import SortedElements from '~/routes/studio/context/sorted-elements' import Search from '~/components/search/search' import { useProjectStore } from '~/stores/project-store' import type { ElementDetails } from '@frankframework/doc-library-core' import { useFFDoc } from '@frankframework/doc-library-react' import LoadingSpinner from '~/components/loading-spinner' +import LoadError from '~/components/load-error' import { useFrankConfigXsd } from '~/providers/frankconfig-xsd-provider' import { getChildrenForType, getFirstLevelElementsForType } from '~/utils/xsd-utils' import { DEFAULT_ELEMENTS, NON_CANVAS_ELEMENTS } from './palette-config' const ROOT_TYPES = ['PipelineType', 'ReceiverType'] +const PALETTE_LOAD_TIMEOUT_MS = 15_000 export default function StudioContext() { const { setDraggedName, setAllowedOnCanvas } = useNodeContextStore((state) => state) const [searchTerm, setSearchTerm] = useState('') const project = useProjectStore((state) => state.project) - const { filters, elements, isLoading } = useFFDoc() - const { xsdDoc } = useFrankConfigXsd() + const { filters, elements, isLoading, error: ffDocError, refetch: refetchFFDoc } = useFFDoc() + const { xsdDoc, error: xsdError, refetch: refetchXsd } = useFrankConfigXsd() const { allowed, elementsAllowedOnCanvas } = useMemo(() => { if (!xsdDoc) return { allowed: null, elementsAllowedOnCanvas: [] } @@ -33,7 +35,33 @@ export default function StudioContext() { } }, [xsdDoc]) - if (isLoading || !elements || allowed === null) { + const isReady = !isLoading && !!elements && allowed !== null + const hasError = ffDocError !== null || xsdError !== null + + const [timedOut, setTimedOut] = useState(false) + + useEffect(() => { + if (isReady || hasError) return + + const timer = setTimeout(() => setTimedOut(true), PALETTE_LOAD_TIMEOUT_MS) + return () => clearTimeout(timer) + }, [isReady, hasError]) + + const retry = () => { + setTimedOut(false) + refetchFFDoc() + refetchXsd() + } + + if (!isReady) { + if (hasError || timedOut) { + return ( +
+ +
+ ) + } + return (