Add loading spinner back to stream just for engine connection, give it an error state (#6220)
* Add an engine error type for an "outage" * Add a loading spinner back to the stream just for engine connection * Refactor Loading spinner to account for early errors * Add styling and state logic for unrecoverable errors in Loading * Let engine error messages contain markdown * Clarify 'too many connections' error message * Add a "VeryLongLoadTime" error that suggests checking firewall * Give the engine connection spinner a test ID and use it
This commit is contained in:
@ -5,7 +5,10 @@ import { useModelingContext } from '@src/hooks/useModelingContext'
|
||||
import { useNetworkContext } from '@src/hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from '@src/hooks/useNetworkStatus'
|
||||
import { getArtifactOfTypes } from '@src/lang/std/artifactGraph'
|
||||
import { EngineCommandManagerEvents } from '@src/lang/std/engineConnection'
|
||||
import {
|
||||
EngineCommandManagerEvents,
|
||||
EngineConnectionStateType,
|
||||
} from '@src/lang/std/engineConnection'
|
||||
import { btnName } from '@src/lib/cameraControls'
|
||||
import { PATHS } from '@src/lib/paths'
|
||||
import { sendSelectEventToEngine } from '@src/lib/selections'
|
||||
@ -25,6 +28,7 @@ import {
|
||||
EngineStreamTransition,
|
||||
} from '@src/machines/engineStreamMachine'
|
||||
|
||||
import Loading from '@src/components/Loading'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import type { MouseEventHandler } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
@ -426,6 +430,12 @@ export const EngineStream = (props: {
|
||||
}
|
||||
menuTargetElement={videoWrapperRef}
|
||||
/>
|
||||
{engineCommandManager.engineConnection?.state.type !==
|
||||
EngineConnectionStateType.ConnectionEstablished && (
|
||||
<Loading dataTestId="loading-engine" className="fixed inset-0">
|
||||
Connecting to engine
|
||||
</Loading>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
import type { MarkedOptions } from '@ts-stack/markdown'
|
||||
import { Marked, escape, unescape } from '@ts-stack/markdown'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { CustomIcon } from '@src/components/CustomIcon'
|
||||
import { Spinner } from '@src/components/Spinner'
|
||||
import type { ErrorType } from '@src/lang/std/engineConnection'
|
||||
import {
|
||||
CONNECTION_ERROR_TEXT,
|
||||
ConnectionError,
|
||||
@ -9,15 +13,33 @@ import {
|
||||
EngineConnectionEvents,
|
||||
EngineConnectionStateType,
|
||||
} from '@src/lang/std/engineConnection'
|
||||
import { SafeRenderer } from '@src/lib/markdown'
|
||||
import { engineCommandManager } from '@src/lib/singletons'
|
||||
|
||||
interface LoadingProps extends React.PropsWithChildren {
|
||||
className?: string
|
||||
dataTestId?: string
|
||||
}
|
||||
|
||||
const Loading = ({ children, className }: LoadingProps) => {
|
||||
const [error, setError] = useState<ConnectionError>(ConnectionError.Unset)
|
||||
const markedOptions: MarkedOptions = {
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
sanitize: true,
|
||||
unescape,
|
||||
escape,
|
||||
}
|
||||
|
||||
const Loading = ({ children, className, dataTestId }: LoadingProps) => {
|
||||
const [error, setError] = useState<ErrorType>({
|
||||
error: ConnectionError.Unset,
|
||||
})
|
||||
const isUnrecoverableError = error.error > ConnectionError.VeryLongLoadingTime
|
||||
const colorClass =
|
||||
error.error === ConnectionError.Unset
|
||||
? 'text-primary'
|
||||
: !isUnrecoverableError
|
||||
? 'text-warn-60'
|
||||
: 'text-chalkboard-60 dark:text-chalkboard-40'
|
||||
useEffect(() => {
|
||||
const onConnectionStateChange = ({ detail: state }: CustomEvent) => {
|
||||
if (
|
||||
@ -26,7 +48,7 @@ const Loading = ({ children, className }: LoadingProps) => {
|
||||
state.value?.type !== DisconnectingType.Error
|
||||
)
|
||||
return
|
||||
setError(state.value.value.error)
|
||||
setError(state.value.value)
|
||||
}
|
||||
|
||||
const onEngineAvailable = ({ detail: engineConnection }: CustomEvent) => {
|
||||
@ -36,10 +58,27 @@ const Loading = ({ children, className }: LoadingProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
engineCommandManager.addEventListener(
|
||||
EngineCommandManagerEvents.EngineAvailable,
|
||||
onEngineAvailable as EventListener
|
||||
)
|
||||
if (engineCommandManager.engineConnection) {
|
||||
// Do an initial state check in case there is an immediate issue
|
||||
onConnectionStateChange(
|
||||
new CustomEvent(EngineConnectionEvents.ConnectionStateChanged, {
|
||||
detail: engineCommandManager.engineConnection.state,
|
||||
})
|
||||
)
|
||||
// Set up a listener on the state for future updates
|
||||
onEngineAvailable(
|
||||
new CustomEvent(EngineCommandManagerEvents.EngineAvailable, {
|
||||
detail: engineCommandManager.engineConnection,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
// If there is no engine connection yet, listen for it to be there *then*
|
||||
// attach the listener
|
||||
engineCommandManager.addEventListener(
|
||||
EngineCommandManagerEvents.EngineAvailable,
|
||||
onEngineAvailable as EventListener
|
||||
)
|
||||
}
|
||||
|
||||
return () => {
|
||||
engineCommandManager.removeEventListener(
|
||||
@ -55,30 +94,59 @@ const Loading = ({ children, className }: LoadingProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
// Don't set long loading time if there's a more severe error
|
||||
if (error > ConnectionError.LongLoadingTime) return
|
||||
if (isUnrecoverableError) return
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setError(ConnectionError.LongLoadingTime)
|
||||
const shorterTimer = setTimeout(() => {
|
||||
setError({ error: ConnectionError.LongLoadingTime })
|
||||
}, 4000)
|
||||
const longerTimer = setTimeout(() => {
|
||||
setError({ error: ConnectionError.VeryLongLoadingTime })
|
||||
}, 7000)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [error, setError])
|
||||
return () => {
|
||||
clearTimeout(shorterTimer)
|
||||
clearTimeout(longerTimer)
|
||||
}
|
||||
}, [error, setError, isUnrecoverableError])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`body-bg flex flex-col items-center justify-center h-screen ${className}`}
|
||||
data-testid="loading"
|
||||
className={`body-bg flex flex-col items-center justify-center h-screen ${colorClass} ${className}`}
|
||||
data-testid={dataTestId ? dataTestId : 'loading'}
|
||||
>
|
||||
<Spinner />
|
||||
<p className="text-base mt-4 text-primary">{children || 'Loading'}</p>
|
||||
<p
|
||||
className={
|
||||
'text-sm mt-4 text-primary/60 transition-opacity duration-500' +
|
||||
(error !== ConnectionError.Unset ? ' opacity-100' : ' opacity-0')
|
||||
}
|
||||
>
|
||||
{CONNECTION_ERROR_TEXT[error]}
|
||||
{isUnrecoverableError ? (
|
||||
<CustomIcon
|
||||
name="close"
|
||||
className="w-8 h-8 !text-chalkboard-10 bg-destroy-60 rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
<p className={`text-base mt-4`}>
|
||||
{isUnrecoverableError ? 'An error occurred' : children || 'Loading'}
|
||||
</p>
|
||||
{CONNECTION_ERROR_TEXT[error.error] && (
|
||||
<div
|
||||
className={
|
||||
'text-center text-sm mt-4 text-opacity-70 transition-opacity duration-500' +
|
||||
(error.error !== ConnectionError.Unset
|
||||
? ' opacity-100'
|
||||
: ' opacity-0')
|
||||
}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Marked.parse(
|
||||
CONNECTION_ERROR_TEXT[error.error] +
|
||||
(error.context
|
||||
? '\n\nThe error details are: ' + error.context
|
||||
: ''),
|
||||
{
|
||||
renderer: new SafeRenderer(markedOptions),
|
||||
...markedOptions,
|
||||
}
|
||||
),
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export const Spinner = (props: SVGProps<SVGSVGElement>) => {
|
||||
cx="5"
|
||||
cy="5"
|
||||
r="4"
|
||||
stroke="var(--primary)"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeDasharray="4, 4"
|
||||
className="animate-spin origin-center"
|
||||
|
Reference in New Issue
Block a user