Show when user can't connect because of a bad token (#2105)
* Reapply "Add ping pong health, remove a timeout interval, fix up netwo… (#1771)
This reverts commit 1913519f68
.
* Fix build errors
* Add new error states to network status notification
* Remove unused variable
* Refactor to use Context API for network status
* Don't do any stream events if network is not ok
* Catch LSP errors on bad auth
* Show when authentication is bad (cookie header only)
* Fix formatting
* Fix up types
* Revert awaiting on lsp failure
* Fix tsc
* wip
* wip
* fmt
* Fix typing
* Incorporate ping health; yarn make:dev; faster video stream loss notice
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* run ci pls
* run ci pls
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* run ci pls again
* Remove unused variables
* Add new instructions on running Playwright anywhere
* Please the Playwright. Praise the Playwright.
* Correct a vitest
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* ci again
* Fix tests unrelated to this PR
* Fix flakiness in for segments tests
* Bump to 2 workers
* fmt
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* fmt
* fmt
* Fixups
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* ci
* Set workers to 1
* Wait for network status listeners before connecting
* Fix initial connection requirements and trying 2 workers again
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@ -1,14 +1,65 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
EngineConnectionStateType,
|
||||
DisconnectingType,
|
||||
EngineCommandManagerEvents,
|
||||
EngineConnectionEvents,
|
||||
ConnectionError,
|
||||
CONNECTION_ERROR_TEXT,
|
||||
} from '../lang/std/engineConnection'
|
||||
|
||||
import { engineCommandManager } from '../lib/singletons'
|
||||
|
||||
const Loading = ({ children }: React.PropsWithChildren) => {
|
||||
const [hasLongLoadTime, setHasLongLoadTime] = useState(false)
|
||||
const [error, setError] = useState<ConnectionError>(ConnectionError.Unset)
|
||||
|
||||
useEffect(() => {
|
||||
const onConnectionStateChange = ({ detail: state }: CustomEvent) => {
|
||||
if (
|
||||
(state.type !== EngineConnectionStateType.Disconnected ||
|
||||
state.type !== EngineConnectionStateType.Disconnecting) &&
|
||||
state.value?.type !== DisconnectingType.Error
|
||||
)
|
||||
return
|
||||
setError(state.value.value.error)
|
||||
}
|
||||
|
||||
const onEngineAvailable = ({ detail: engineConnection }: CustomEvent) => {
|
||||
engineConnection.addEventListener(
|
||||
EngineConnectionEvents.ConnectionStateChanged,
|
||||
onConnectionStateChange as EventListener
|
||||
)
|
||||
}
|
||||
|
||||
engineCommandManager.addEventListener(
|
||||
EngineCommandManagerEvents.EngineAvailable,
|
||||
onEngineAvailable as EventListener
|
||||
)
|
||||
|
||||
return () => {
|
||||
engineCommandManager.removeEventListener(
|
||||
EngineCommandManagerEvents.EngineAvailable,
|
||||
onEngineAvailable as EventListener
|
||||
)
|
||||
engineCommandManager.engineConnection?.removeEventListener(
|
||||
EngineConnectionEvents.ConnectionStateChanged,
|
||||
onConnectionStateChange as EventListener
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Don't set long loading time if there's a more severe error
|
||||
if (error > ConnectionError.LongLoadingTime) return
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setHasLongLoadTime(true)
|
||||
setError(ConnectionError.LongLoadingTime)
|
||||
}, 4000)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [setHasLongLoadTime])
|
||||
}, [error, setError])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="body-bg flex flex-col items-center justify-center h-screen"
|
||||
@ -29,10 +80,10 @@ const Loading = ({ children }: React.PropsWithChildren) => {
|
||||
<p
|
||||
className={
|
||||
'text-sm mt-4 text-primary/60 transition-opacity duration-500' +
|
||||
(hasLongLoadTime ? ' opacity-100' : ' opacity-0')
|
||||
(error !== ConnectionError.Unset ? ' opacity-100' : ' opacity-0')
|
||||
}
|
||||
>
|
||||
Loading is taking longer than expected.
|
||||
{CONNECTION_ERROR_TEXT[error]}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
@ -13,7 +13,6 @@ import { LanguageSupport } from '@codemirror/language'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { paths } from 'lib/paths'
|
||||
import { FileEntry } from 'lib/types'
|
||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
||||
import Worker from 'editor/plugins/lsp/worker.ts?worker'
|
||||
import {
|
||||
LspWorkerEventType,
|
||||
@ -23,6 +22,8 @@ import {
|
||||
} from 'editor/plugins/lsp/types'
|
||||
import { wasmUrl } from 'lang/wasm'
|
||||
import { PROJECT_ENTRYPOINT } from 'lib/constants'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
|
||||
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
|
||||
return []
|
||||
@ -86,7 +87,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
} = useSettingsAuthContext()
|
||||
const token = auth?.context.token
|
||||
const navigate = useNavigate()
|
||||
const { overallState } = useNetworkStatus()
|
||||
const { overallState } = useNetworkContext()
|
||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||
|
||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
||||
|
@ -5,8 +5,8 @@ import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
||||
import {
|
||||
NETWORK_HEALTH_TEXT,
|
||||
NetworkHealthIndicator,
|
||||
NetworkHealthState,
|
||||
} from './NetworkHealthIndicator'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
|
||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
// wrap in router and xState context
|
||||
@ -19,6 +19,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
)
|
||||
}
|
||||
|
||||
// Our Playwright tests for this are much more comprehensive.
|
||||
describe('NetworkHealthIndicator tests', () => {
|
||||
test('Renders the network indicator', () => {
|
||||
render(
|
||||
@ -29,21 +30,7 @@ describe('NetworkHealthIndicator tests', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||
|
||||
expect(screen.getByTestId('network')).toHaveTextContent(
|
||||
NETWORK_HEALTH_TEXT[NetworkHealthState.Ok]
|
||||
)
|
||||
})
|
||||
|
||||
test('Responds to network changes', () => {
|
||||
render(
|
||||
<TestWrap>
|
||||
<NetworkHealthIndicator />
|
||||
</TestWrap>
|
||||
)
|
||||
|
||||
fireEvent.offline(window)
|
||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||
|
||||
// Starts as disconnected
|
||||
expect(screen.getByTestId('network')).toHaveTextContent(
|
||||
NETWORK_HEALTH_TEXT[NetworkHealthState.Disconnected]
|
||||
)
|
||||
|
@ -1,26 +1,13 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
||||
import {
|
||||
ConnectingType,
|
||||
ConnectingTypeGroup,
|
||||
DisconnectingType,
|
||||
EngineConnectionState,
|
||||
EngineConnectionStateType,
|
||||
ErrorType,
|
||||
initialConnectingTypeGroupState,
|
||||
} from '../lang/std/engineConnection'
|
||||
import { engineCommandManager } from '../lib/singletons'
|
||||
import Tooltip from './Tooltip'
|
||||
|
||||
export enum NetworkHealthState {
|
||||
Ok,
|
||||
Issue,
|
||||
Disconnected,
|
||||
}
|
||||
import { ConnectingTypeGroup } from '../lang/std/engineConnection'
|
||||
import { useNetworkContext } from '../hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from '../hooks/useNetworkStatus'
|
||||
|
||||
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
|
||||
[NetworkHealthState.Ok]: 'Connected',
|
||||
[NetworkHealthState.Weak]: 'Weak',
|
||||
[NetworkHealthState.Issue]: 'Problem',
|
||||
[NetworkHealthState.Disconnected]: 'Offline',
|
||||
}
|
||||
@ -61,6 +48,10 @@ const overallConnectionStateColor: Record<NetworkHealthState, IconColorConfig> =
|
||||
icon: 'text-succeed-80 dark:text-succeed-10',
|
||||
bg: 'bg-succeed-10/30 dark:bg-succeed-80/50',
|
||||
},
|
||||
[NetworkHealthState.Weak]: {
|
||||
icon: 'text-warn-80 dark:text-warn-10',
|
||||
bg: 'bg-warn-10 dark:bg-warn-80/80',
|
||||
},
|
||||
[NetworkHealthState.Issue]: {
|
||||
icon: 'text-destroy-80 dark:text-destroy-10',
|
||||
bg: 'bg-destroy-10 dark:bg-destroy-80/80',
|
||||
@ -76,125 +67,11 @@ const overallConnectionStateIcon: Record<
|
||||
ActionIconProps['icon']
|
||||
> = {
|
||||
[NetworkHealthState.Ok]: 'network',
|
||||
[NetworkHealthState.Weak]: 'network',
|
||||
[NetworkHealthState.Issue]: 'networkCrossedOut',
|
||||
[NetworkHealthState.Disconnected]: 'networkCrossedOut',
|
||||
}
|
||||
|
||||
export function useNetworkStatus() {
|
||||
const [steps, setSteps] = useState(initialConnectingTypeGroupState)
|
||||
const [internetConnected, setInternetConnected] = useState<boolean>(true)
|
||||
const [overallState, setOverallState] = useState<NetworkHealthState>(
|
||||
NetworkHealthState.Ok
|
||||
)
|
||||
const [hasCopied, setHasCopied] = useState<boolean>(false)
|
||||
|
||||
const [error, setError] = useState<ErrorType | undefined>(undefined)
|
||||
|
||||
const issues: Record<ConnectingTypeGroup, boolean> = {
|
||||
[ConnectingTypeGroup.WebSocket]: steps[ConnectingTypeGroup.WebSocket].some(
|
||||
(a: [ConnectingType, boolean | undefined]) => a[1] === false
|
||||
),
|
||||
[ConnectingTypeGroup.ICE]: steps[ConnectingTypeGroup.ICE].some(
|
||||
(a: [ConnectingType, boolean | undefined]) => a[1] === false
|
||||
),
|
||||
[ConnectingTypeGroup.WebRTC]: steps[ConnectingTypeGroup.WebRTC].some(
|
||||
(a: [ConnectingType, boolean | undefined]) => a[1] === false
|
||||
),
|
||||
}
|
||||
|
||||
const hasIssues: boolean =
|
||||
issues[ConnectingTypeGroup.WebSocket] ||
|
||||
issues[ConnectingTypeGroup.ICE] ||
|
||||
issues[ConnectingTypeGroup.WebRTC]
|
||||
|
||||
useEffect(() => {
|
||||
setOverallState(
|
||||
!internetConnected
|
||||
? NetworkHealthState.Disconnected
|
||||
: hasIssues
|
||||
? NetworkHealthState.Issue
|
||||
: NetworkHealthState.Ok
|
||||
)
|
||||
}, [hasIssues, internetConnected])
|
||||
|
||||
useEffect(() => {
|
||||
const onlineCallback = () => {
|
||||
setSteps(initialConnectingTypeGroupState)
|
||||
setInternetConnected(true)
|
||||
}
|
||||
const offlineCallback = () => {
|
||||
setInternetConnected(false)
|
||||
}
|
||||
window.addEventListener('online', onlineCallback)
|
||||
window.addEventListener('offline', offlineCallback)
|
||||
return () => {
|
||||
window.removeEventListener('online', onlineCallback)
|
||||
window.removeEventListener('offline', offlineCallback)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
engineCommandManager.onConnectionStateChange(
|
||||
(engineConnectionState: EngineConnectionState) => {
|
||||
let hasSetAStep = false
|
||||
|
||||
if (
|
||||
engineConnectionState.type === EngineConnectionStateType.Connecting
|
||||
) {
|
||||
const groups = Object.values(steps)
|
||||
for (let group of groups) {
|
||||
for (let step of group) {
|
||||
if (step[0] !== engineConnectionState.value.type) continue
|
||||
step[1] = true
|
||||
hasSetAStep = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
engineConnectionState.type === EngineConnectionStateType.Disconnecting
|
||||
) {
|
||||
const groups = Object.values(steps)
|
||||
for (let group of groups) {
|
||||
for (let step of group) {
|
||||
if (
|
||||
engineConnectionState.value.type === DisconnectingType.Error
|
||||
) {
|
||||
if (
|
||||
engineConnectionState.value.value.lastConnectingValue
|
||||
?.type === step[0]
|
||||
) {
|
||||
step[1] = false
|
||||
hasSetAStep = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (engineConnectionState.value.type === DisconnectingType.Error) {
|
||||
setError(engineConnectionState.value.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSetAStep) {
|
||||
setSteps(steps)
|
||||
}
|
||||
}
|
||||
)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
hasIssues,
|
||||
overallState,
|
||||
internetConnected,
|
||||
steps,
|
||||
issues,
|
||||
error,
|
||||
setHasCopied,
|
||||
hasCopied,
|
||||
}
|
||||
}
|
||||
|
||||
export const NetworkHealthIndicator = () => {
|
||||
const {
|
||||
hasIssues,
|
||||
@ -205,7 +82,7 @@ export const NetworkHealthIndicator = () => {
|
||||
error,
|
||||
setHasCopied,
|
||||
hasCopied,
|
||||
} = useNetworkStatus()
|
||||
} = useNetworkContext()
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
@ -259,18 +136,18 @@ export const NetworkHealthIndicator = () => {
|
||||
size="lg"
|
||||
icon={
|
||||
hasIssueToIcon[
|
||||
issues[name as ConnectingTypeGroup].toString()
|
||||
String(issues[name as ConnectingTypeGroup])
|
||||
]
|
||||
}
|
||||
iconClassName={
|
||||
hasIssueToIconColors[
|
||||
issues[name as ConnectingTypeGroup].toString()
|
||||
String(issues[name as ConnectingTypeGroup])
|
||||
].icon
|
||||
}
|
||||
bgClassName={
|
||||
'rounded-sm ' +
|
||||
hasIssueToIconColors[
|
||||
issues[name as ConnectingTypeGroup].toString()
|
||||
String(issues[name as ConnectingTypeGroup])
|
||||
].bg
|
||||
}
|
||||
/>
|
||||
|
@ -5,7 +5,6 @@ import { paths } from 'lib/paths'
|
||||
import { isTauri } from '../lib/isTauri'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Fragment } from 'react'
|
||||
import { FileTree } from './FileTree'
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
import { Logo } from './Logo'
|
||||
import { APP_NAME } from 'lib/constants'
|
||||
|
@ -4,8 +4,9 @@ import { getNormalisedCoordinates } from '../lib/utils'
|
||||
import Loading from './Loading'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
||||
import { butName } from 'lib/cameraControls'
|
||||
import { sendSelectEventToEngine } from 'lib/selections'
|
||||
|
||||
@ -28,8 +29,11 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
}))
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const { state } = useModelingContext()
|
||||
const { overallState } = useNetworkStatus()
|
||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||
const { overallState } = useNetworkContext()
|
||||
|
||||
const isNetworkOkay =
|
||||
overallState === NetworkHealthState.Ok ||
|
||||
overallState === NetworkHealthState.Weak
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -43,6 +47,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
}, [mediaStream])
|
||||
|
||||
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
if (!isNetworkOkay) return
|
||||
if (!videoRef.current) return
|
||||
if (state.matches('Sketch')) return
|
||||
if (state.matches('Sketch no face')) return
|
||||
@ -58,6 +63,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
}
|
||||
|
||||
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
if (!isNetworkOkay) return
|
||||
if (!videoRef.current) return
|
||||
setButtonDownInStream(undefined)
|
||||
if (state.matches('Sketch')) return
|
||||
@ -72,6 +78,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
}
|
||||
|
||||
const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => {
|
||||
if (!isNetworkOkay) return
|
||||
if (state.matches('Sketch')) return
|
||||
if (state.matches('Sketch no face')) return
|
||||
if (!clickCoords) return
|
||||
@ -112,7 +119,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
{!isNetworkOkay && !isLoading && (
|
||||
<div className="text-center absolute inset-0">
|
||||
<Loading>
|
||||
<span data-testid="loading-stream">Stream disconnected</span>
|
||||
<span data-testid="loading-stream">Stream disconnected...</span>
|
||||
</Loading>
|
||||
</div>
|
||||
)}
|
||||
|
Reference in New Issue
Block a user