Propagate errors UI (#1369)
* Pass engine connection state to NetworkHealthIndicator * Create the basis for styling and further work * Add icons * Update styles on network health indicator * Cleanup styles and unused state * Rename State to NetworkHealthState * Update tests * fmt --------- Co-authored-by: 49lf <ircsurfer33@gmail.com>
This commit is contained in:
@ -4,6 +4,8 @@ export type CustomIconName =
|
||||
| 'arrowRight'
|
||||
| 'arrowUp'
|
||||
| 'checkmark'
|
||||
| 'clipboardPlus'
|
||||
| 'clipboardCheckmark'
|
||||
| 'close'
|
||||
| 'equal'
|
||||
| 'extrude'
|
||||
@ -13,8 +15,11 @@ export type CustomIconName =
|
||||
| 'folderPlus'
|
||||
| 'gear'
|
||||
| 'horizontal'
|
||||
| 'horizontalDash'
|
||||
| 'line'
|
||||
| 'move'
|
||||
| 'network'
|
||||
| 'networkCrossedOut'
|
||||
| 'parallel'
|
||||
| 'search'
|
||||
| 'sketch'
|
||||
@ -107,6 +112,38 @@ export const CustomIcon = ({
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'clipboardCheckmark':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.5 3H7L13 3L13.5 3V3.5V4.00001L15.5 4.00002L16 4.00002V4.50002V10.0351C15.6905 9.85609 15.3548 9.71733 15 9.62602V5.00002L13.5 5.00001V6.50001V7.00001L13 7.00001L7 7.00001L6.5 7.00001V6.50001V5.00001L5 5.00001V16H10.8773C11.2024 16.4055 11.6047 16.7463 12.062 17H4.5H4V16.5V4.50001V4.00001L4.5 4.00001L6.5 4.00001V3.5V3ZM15.938 17C15.9588 16.9885 15.9794 16.9768 16 16.9649V17H15.938ZM7.5 4V4.50001V6.00001L12.5 6.00001V4.50001V4L7.5 4ZM13 9H7V8H13V9ZM15.6855 11.5L13.2101 14.8005L12.2071 13.7975L11.5 14.5046L12.9107 15.9153L13.6642 15.8617L16.4855 12.1L15.6855 11.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'clipboardPlus':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.5 3H7L13 3L13.5 3V3.5V4.00001L15.5 4.00002L16 4.00002V4.50002V10.0351C15.6905 9.85609 15.3548 9.71733 15 9.62602V5.00002L13.5 5.00001V6.50001V7.00001L13 7.00001L7 7.00001L6.5 7.00001V6.50001V5.00001L5 5.00001V16H10.8773C11.2024 16.4055 11.6047 16.7463 12.062 17H4.5H4V16.5V4.50001V4.00001L4.5 4.00001L6.5 4.00001V3.5V3ZM15.938 17C15.9588 16.9885 15.9794 16.9768 16 16.9649V17H15.938ZM7.5 4V4.50001V6.00001L12.5 6.00001V4.50001V4L7.5 4ZM13 9H7V8H13V9ZM13.5 11V13H11.5V14H13.5V16H14.5V14H16.5V13H14.5V11H13.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'close':
|
||||
return (
|
||||
<svg
|
||||
@ -249,6 +286,22 @@ export const CustomIcon = ({
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'horizontalDash':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14 10.5H6V9.5H14V10.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'line':
|
||||
return (
|
||||
<svg
|
||||
@ -281,6 +334,38 @@ export const CustomIcon = ({
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'network':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M18 9.64741C17.1925 8.24871 16.0344 7.08457 14.6399 6.26971C13.2455 5.45486 11.6628 5.01742 10.0478 5.00051C8.4328 4.9836 6.84127 5.38779 5.43006 6.17326C4.01884 6.95873 2.83666 8.09837 2 9.47985L2.76881 9.94546C3.52456 8.69756 4.59243 7.66813 5.86718 6.95862C7.14193 6.2491 8.57955 5.88399 10.0384 5.89927C11.4972 5.91455 12.9269 6.30968 14.1865 7.04574C15.4461 7.7818 16.4922 8.83337 17.2216 10.0968L18 9.64741ZM15.2155 11.0953C14.6772 10.1628 13.9051 9.3867 12.9755 8.84347C12.0459 8.30023 10.9907 8.00861 9.91406 7.99733C8.8374 7.98606 7.77638 8.25552 6.83557 8.77917C5.89476 9.30281 5.10664 10.0626 4.54887 10.9836L5.34391 11.4651C5.81802 10.6822 6.48792 10.0364 7.28761 9.59132C8.0873 9.14622 8.98916 8.91718 9.90432 8.92676C10.8195 8.93635 11.7164 9.18423 12.5065 9.64598C13.2967 10.1077 13.953 10.7674 14.4106 11.56L15.2155 11.0953ZM10 14C10.8284 14 11.5 13.3284 11.5 12.5C11.5 11.6716 10.8284 11 10 11C9.17157 11 8.5 11.6716 8.5 12.5C8.5 13.3284 9.17157 14 10 14Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'networkCrossedOut':
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.35352 5.39647L14.253 15.296L14.9601 14.5889L5.06062 4.68936L4.35352 5.39647ZM12.5065 9.64599C11.9609 9.32713 11.3643 9.11025 10.746 9.00341L9.74058 7.99796C9.79835 7.99694 9.85618 7.99674 9.91406 7.99735C10.9907 8.00862 12.0459 8.30025 12.9755 8.84348C13.9051 9.38672 14.6772 10.1628 15.2155 11.0953L14.4106 11.56C13.953 10.7674 13.2967 10.1077 12.5065 9.64599ZM6.48788 8.98789L7.16295 9.66297C6.41824 10.1045 5.79317 10.7233 5.34391 11.4651L4.54887 10.9836C5.03646 10.1785 5.70009 9.49656 6.48788 8.98789ZM10.0384 5.89928C9.3134 5.89169 8.59366 5.97804 7.89655 6.15392L7.16867 5.42605C8.09637 5.13507 9.06776 4.99026 10.0478 5.00052C11.6628 5.01744 13.2455 5.45488 14.6399 6.26973C16.0344 7.08458 17.1925 8.24872 18 9.64742L17.2216 10.0968C16.4922 8.83338 15.4461 7.78181 14.1865 7.04575C12.9269 6.3097 11.4972 5.91456 10.0384 5.89928ZM5.00782 7.50783L4.36522 6.86524C3.42033 7.57557 2.61639 8.46208 2 9.47986L2.76881 9.94547C3.34775 8.98952 4.10986 8.16177 5.00782 7.50783ZM10 14C10.4142 14 10.7892 13.8321 11.0607 13.5607L8.93934 11.4394C8.66789 11.7108 8.5 12.0858 8.5 12.5C8.5 13.3284 9.17157 14 10 14Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
case 'parallel':
|
||||
return (
|
||||
<svg
|
||||
|
@ -3,8 +3,9 @@ import { BrowserRouter } from 'react-router-dom'
|
||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||
import CommandBarProvider from './CommandBar/CommandBar'
|
||||
import {
|
||||
NETWORK_CONTENT,
|
||||
NETWORK_HEALTH_TEXT,
|
||||
NetworkHealthIndicator,
|
||||
NetworkHealthState,
|
||||
} from './NetworkHealthIndicator'
|
||||
|
||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||
@ -28,8 +29,8 @@ describe('NetworkHealthIndicator tests', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||
|
||||
expect(screen.getByTestId('network-good')).toHaveTextContent(
|
||||
NETWORK_CONTENT.good
|
||||
expect(screen.getByTestId('network')).toHaveTextContent(
|
||||
NETWORK_HEALTH_TEXT[NetworkHealthState.Ok]
|
||||
)
|
||||
})
|
||||
|
||||
@ -43,8 +44,8 @@ describe('NetworkHealthIndicator tests', () => {
|
||||
fireEvent.offline(window)
|
||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||
|
||||
expect(screen.getByTestId('network-bad')).toHaveTextContent(
|
||||
NETWORK_CONTENT.bad
|
||||
expect(screen.getByTestId('network')).toHaveTextContent(
|
||||
NETWORK_HEALTH_TEXT[NetworkHealthState.Disconnected]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -1,41 +1,186 @@
|
||||
import { faExclamation, faWifi } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ActionIcon } from './ActionIcon'
|
||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
||||
import {
|
||||
ConnectingType,
|
||||
ConnectingTypeGroup,
|
||||
DisconnectingType,
|
||||
engineCommandManager,
|
||||
EngineConnectionState,
|
||||
EngineConnectionStateType,
|
||||
ErrorType,
|
||||
initialConnectingTypeGroupState,
|
||||
} from '../lang/std/engineConnection'
|
||||
import Tooltip from './Tooltip'
|
||||
|
||||
export const NETWORK_CONTENT = {
|
||||
good: 'Network health is good',
|
||||
bad: 'Network issue',
|
||||
export enum NetworkHealthState {
|
||||
Ok,
|
||||
Issue,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
const NETWORK_MESSAGES = {
|
||||
offline: 'You are offline',
|
||||
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
|
||||
[NetworkHealthState.Ok]: 'Connected',
|
||||
[NetworkHealthState.Issue]: 'Problem',
|
||||
[NetworkHealthState.Disconnected]: 'Offline',
|
||||
}
|
||||
|
||||
type IconColorConfig = {
|
||||
icon: string
|
||||
bg: string
|
||||
}
|
||||
|
||||
const hasIssueToIcon: Record<
|
||||
string | number | symbol,
|
||||
ActionIconProps['icon']
|
||||
> = {
|
||||
true: 'close',
|
||||
undefined: 'horizontalDash',
|
||||
false: 'checkmark',
|
||||
}
|
||||
|
||||
const hasIssueToIconColors: Record<string | number | symbol, IconColorConfig> =
|
||||
{
|
||||
true: {
|
||||
icon: 'text-destroy-80 dark:text-destroy-10',
|
||||
bg: 'bg-destroy-10 dark:bg-destroy-80',
|
||||
},
|
||||
undefined: {
|
||||
icon: 'text-chalkboard-70 dark:text-chalkboard-30',
|
||||
bg: 'bg-chalkboard-30 dark:bg-chalkboard-70',
|
||||
},
|
||||
false: {
|
||||
icon: 'text-chalkboard-110 dark:!text-chalkboard-10',
|
||||
bg: 'bg-transparent dark:bg-transparent',
|
||||
},
|
||||
}
|
||||
|
||||
const overallConnectionStateColor: Record<NetworkHealthState, IconColorConfig> =
|
||||
{
|
||||
[NetworkHealthState.Ok]: {
|
||||
icon: 'text-energy-80 dark:text-energy-10',
|
||||
bg: 'bg-energy-10/30 dark:bg-energy-80/50',
|
||||
},
|
||||
[NetworkHealthState.Issue]: {
|
||||
icon: 'text-destroy-80 dark:text-destroy-10',
|
||||
bg: 'bg-destroy-10 dark:bg-destroy-80/80',
|
||||
},
|
||||
[NetworkHealthState.Disconnected]: {
|
||||
icon: 'text-destroy-80 dark:text-destroy-10',
|
||||
bg: 'bg-destroy-10 dark:bg-destroy-80',
|
||||
},
|
||||
}
|
||||
|
||||
const overallConnectionStateIcon: Record<
|
||||
NetworkHealthState,
|
||||
ActionIconProps['icon']
|
||||
> = {
|
||||
[NetworkHealthState.Ok]: 'network',
|
||||
[NetworkHealthState.Issue]: 'networkCrossedOut',
|
||||
[NetworkHealthState.Disconnected]: 'networkCrossedOut',
|
||||
}
|
||||
|
||||
export const NetworkHealthIndicator = () => {
|
||||
const [networkIssues, setNetworkIssues] = useState<string[]>([])
|
||||
const hasIssues = [...networkIssues.values()].length > 0
|
||||
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(() => {
|
||||
const offlineListener = () =>
|
||||
setNetworkIssues((issues) => {
|
||||
return [
|
||||
...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline),
|
||||
NETWORK_MESSAGES.offline,
|
||||
]
|
||||
})
|
||||
window.addEventListener('offline', offlineListener)
|
||||
setOverallState(
|
||||
!internetConnected
|
||||
? NetworkHealthState.Disconnected
|
||||
: hasIssues
|
||||
? NetworkHealthState.Issue
|
||||
: NetworkHealthState.Ok
|
||||
)
|
||||
}, [hasIssues, internetConnected])
|
||||
|
||||
const onlineListener = () =>
|
||||
setNetworkIssues((issues) => {
|
||||
return [...issues.filter((issue) => issue !== NETWORK_MESSAGES.offline)]
|
||||
})
|
||||
window.addEventListener('online', onlineListener)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('offline', offlineListener)
|
||||
window.removeEventListener('online', onlineListener)
|
||||
useEffect(() => {
|
||||
const cb1 = () => {
|
||||
setSteps(initialConnectingTypeGroupState)
|
||||
setInternetConnected(true)
|
||||
}
|
||||
const cb2 = () => {
|
||||
setInternetConnected(false)
|
||||
}
|
||||
window.addEventListener('online', cb1)
|
||||
window.addEventListener('offline', cb2)
|
||||
return () => {
|
||||
window.removeEventListener('online', cb1)
|
||||
window.removeEventListener('offline', cb2)
|
||||
}
|
||||
}, [])
|
||||
|
||||
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 (
|
||||
@ -45,65 +190,94 @@ export const NetworkHealthIndicator = () => {
|
||||
'p-0 border-none bg-transparent dark:bg-transparent relative ' +
|
||||
(hasIssues
|
||||
? 'focus-visible:outline-destroy-80'
|
||||
: 'focus-visible:outline-succeed-80')
|
||||
: 'focus-visible:outline-energy-80')
|
||||
}
|
||||
data-testid="network-toggle"
|
||||
>
|
||||
<span className="sr-only">Network Health</span>
|
||||
<ActionIcon
|
||||
icon={faWifi}
|
||||
icon={overallConnectionStateIcon[overallState]}
|
||||
className="p-1"
|
||||
iconClassName={overallConnectionStateColor[overallState].icon}
|
||||
bgClassName={
|
||||
'rounded-sm ' + overallConnectionStateColor[overallState].bg
|
||||
}
|
||||
/>
|
||||
<Tooltip position="blockEnd" delay={750} className="ui-open:hidden">
|
||||
Network Health ({NETWORK_HEALTH_TEXT[overallState]})
|
||||
</Tooltip>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="absolute right-0 left-auto top-full mt-1 w-64 flex flex-col gap-1 align-stretch bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm">
|
||||
<div
|
||||
className={`flex items-center justify-between p-2 rounded-t-sm ${overallConnectionStateColor[overallState].bg} ${overallConnectionStateColor[overallState].icon}`}
|
||||
>
|
||||
<h2 className="text-sm font-sans font-normal">Network health</h2>
|
||||
<p
|
||||
data-testid="network"
|
||||
className="font-bold text-xs uppercase px-2 py-1 rounded-sm"
|
||||
>
|
||||
{NETWORK_HEALTH_TEXT[overallState]}
|
||||
</p>
|
||||
</div>
|
||||
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
|
||||
{Object.keys(steps).map((name) => (
|
||||
<li
|
||||
key={name}
|
||||
className={'flex flex-col px-2 py-4 gap-1 last:mb-0 '}
|
||||
>
|
||||
<div className="flex items-center text-left gap-1">
|
||||
<p className="flex-1">{name}</p>
|
||||
{internetConnected ? (
|
||||
<ActionIcon
|
||||
size="lg"
|
||||
icon={
|
||||
hasIssueToIcon[
|
||||
issues[name as ConnectingTypeGroup].toString()
|
||||
]
|
||||
}
|
||||
iconClassName={
|
||||
hasIssues
|
||||
? 'text-destroy-80 dark:text-destroy-30'
|
||||
: 'text-succeed-80 dark:text-succeed-30'
|
||||
hasIssueToIconColors[
|
||||
issues[name as ConnectingTypeGroup].toString()
|
||||
].icon
|
||||
}
|
||||
bgClassName={
|
||||
'bg-transparent dark:bg-transparent ' +
|
||||
(hasIssues
|
||||
? 'hover:bg-destroy-10/50 hover:dark:bg-destroy-80/50 rounded'
|
||||
: 'hover:bg-succeed-10/50 hover:dark:bg-succeed-80/50 rounded')
|
||||
'rounded-sm ' +
|
||||
hasIssueToIconColors[
|
||||
issues[name as ConnectingTypeGroup].toString()
|
||||
].bg
|
||||
}
|
||||
/>
|
||||
</Popover.Button>
|
||||
<Popover.Panel className="absolute right-0 left-auto top-full mt-1 w-56 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch py-2 bg-chalkboard-10 dark:bg-chalkboard-90 rounded shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50 text-sm">
|
||||
{!hasIssues ? (
|
||||
<span
|
||||
className="flex items-center justify-center gap-1 px-4"
|
||||
data-testid="network-good"
|
||||
>
|
||||
<ActionIcon
|
||||
icon="checkmark"
|
||||
bgClassName={'bg-succeed-10/50 dark:bg-succeed-80/50 rounded-sm'}
|
||||
iconClassName={'text-succeed-80 dark:text-succeed-30'}
|
||||
/>
|
||||
{NETWORK_CONTENT.good}
|
||||
</span>
|
||||
) : (
|
||||
<ul className="divide-y divide-chalkboard-20 dark:divide-chalkboard-80">
|
||||
<span
|
||||
className="font-bold text-xs uppercase text-destroy-60 dark:text-destroy-50 px-4"
|
||||
data-testid="network-bad"
|
||||
>
|
||||
{NETWORK_CONTENT.bad}
|
||||
{networkIssues.length > 1 ? 's' : ''}
|
||||
</span>
|
||||
{networkIssues.map((issue) => (
|
||||
<li
|
||||
key={issue}
|
||||
className="flex items-center gap-1 py-2 my-2 last:mb-0"
|
||||
>
|
||||
<ActionIcon
|
||||
icon={faExclamation}
|
||||
bgClassName={'bg-destroy-10/50 dark:bg-destroy-80/50 rounded'}
|
||||
iconClassName={'text-destroy-80 dark:text-destroy-30'}
|
||||
className="ml-4"
|
||||
icon={hasIssueToIcon.true}
|
||||
bgClassName={hasIssueToIconColors.true.bg}
|
||||
iconClassName={hasIssueToIconColors.true.icon}
|
||||
/>
|
||||
<p className="flex-1 mr-4">{issue}</p>
|
||||
)}
|
||||
</div>
|
||||
{issues[name as ConnectingTypeGroup] && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(
|
||||
JSON.stringify(error, null, 2) || ''
|
||||
)
|
||||
setHasCopied(true)
|
||||
setTimeout(() => setHasCopied(false), 5000)
|
||||
}}
|
||||
className="flex w-fit gap-2 items-center bg-transparent text-sm p-1 py-0 my-0 -mx-1 text-destroy-80 dark:text-destroy-10 hover:bg-transparent border-transparent dark:border-transparent hover:border-destroy-80 dark:hover:border-destroy-80 dark:hover:bg-destroy-80"
|
||||
>
|
||||
{hasCopied ? 'Copied' : 'Copy Error'}
|
||||
<ActionIcon
|
||||
size="lg"
|
||||
icon={hasCopied ? 'clipboardCheckmark' : 'clipboardPlus'}
|
||||
iconClassName="text-inherit dark:text-inherit"
|
||||
bgClassName="!bg-transparent"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
)
|
||||
|
@ -58,26 +58,36 @@ type Value<T, U> = U extends undefined
|
||||
|
||||
type State<T, U> = Value<T, U>
|
||||
|
||||
enum EngineConnectionStateType {
|
||||
export enum EngineConnectionStateType {
|
||||
Fresh = 'fresh',
|
||||
Connecting = 'connecting',
|
||||
ConnectionEstablished = 'connection-established',
|
||||
Disconnecting = 'disconnecting',
|
||||
Disconnected = 'disconnected',
|
||||
}
|
||||
|
||||
enum DisconnectedType {
|
||||
export enum DisconnectingType {
|
||||
Error = 'error',
|
||||
Timeout = 'timeout',
|
||||
Quit = 'quit',
|
||||
}
|
||||
|
||||
type DisconnectedValue =
|
||||
| State<DisconnectedType.Error, Error | undefined>
|
||||
| State<DisconnectedType.Timeout, void>
|
||||
| State<DisconnectedType.Quit, void>
|
||||
export interface ErrorType {
|
||||
// We may not necessary have an error to assign.
|
||||
error?: Error
|
||||
|
||||
// We assign this in the state setter because we may have not failed at
|
||||
// a Connecting state, which we check for there.
|
||||
lastConnectingValue?: ConnectingValue
|
||||
}
|
||||
|
||||
export type DisconnectingValue =
|
||||
| State<DisconnectingType.Error, ErrorType>
|
||||
| State<DisconnectingType.Timeout, void>
|
||||
| State<DisconnectingType.Quit, void>
|
||||
|
||||
// These are ordered by the expected sequence.
|
||||
enum ConnectingType {
|
||||
export enum ConnectingType {
|
||||
WebSocketConnecting = 'websocket-connecting',
|
||||
WebSocketEstablished = 'websocket-established',
|
||||
PeerConnectionCreated = 'peer-connection-created',
|
||||
@ -94,7 +104,39 @@ enum ConnectingType {
|
||||
DataChannelEstablished = 'data-channel-established',
|
||||
}
|
||||
|
||||
type ConnectingValue =
|
||||
export enum ConnectingTypeGroup {
|
||||
WebSocket = 'WebSocket',
|
||||
ICE = 'ICE',
|
||||
WebRTC = 'WebRTC',
|
||||
}
|
||||
|
||||
export const initialConnectingTypeGroupState: Record<
|
||||
ConnectingTypeGroup,
|
||||
[ConnectingType, boolean | undefined][]
|
||||
> = {
|
||||
[ConnectingTypeGroup.WebSocket]: [
|
||||
[ConnectingType.WebSocketConnecting, undefined],
|
||||
[ConnectingType.WebSocketEstablished, undefined],
|
||||
],
|
||||
[ConnectingTypeGroup.ICE]: [
|
||||
[ConnectingType.PeerConnectionCreated, undefined],
|
||||
[ConnectingType.ICEServersSet, undefined],
|
||||
[ConnectingType.SetLocalDescription, undefined],
|
||||
[ConnectingType.OfferedSdp, undefined],
|
||||
[ConnectingType.ReceivedSdp, undefined],
|
||||
[ConnectingType.SetRemoteDescription, undefined],
|
||||
[ConnectingType.WebRTCConnecting, undefined],
|
||||
[ConnectingType.ICECandidateReceived, undefined],
|
||||
],
|
||||
[ConnectingTypeGroup.WebRTC]: [
|
||||
[ConnectingType.TrackReceived, undefined],
|
||||
[ConnectingType.DataChannelRequested, undefined],
|
||||
[ConnectingType.DataChannelConnecting, undefined],
|
||||
[ConnectingType.DataChannelEstablished, undefined],
|
||||
],
|
||||
}
|
||||
|
||||
export type ConnectingValue =
|
||||
| State<ConnectingType.WebSocketConnecting, void>
|
||||
| State<ConnectingType.WebSocketEstablished, void>
|
||||
| State<ConnectingType.PeerConnectionCreated, void>
|
||||
@ -110,11 +152,12 @@ type ConnectingValue =
|
||||
| State<ConnectingType.DataChannelConnecting, string>
|
||||
| State<ConnectingType.DataChannelEstablished, void>
|
||||
|
||||
type EngineConnectionState =
|
||||
export type EngineConnectionState =
|
||||
| State<EngineConnectionStateType.Fresh, void>
|
||||
| State<EngineConnectionStateType.Connecting, ConnectingValue>
|
||||
| State<EngineConnectionStateType.ConnectionEstablished, void>
|
||||
| State<EngineConnectionStateType.Disconnected, DisconnectedValue>
|
||||
| State<EngineConnectionStateType.Disconnecting, DisconnectingValue>
|
||||
| State<EngineConnectionStateType.Disconnected, void>
|
||||
|
||||
// EngineConnection encapsulates the connection(s) to the Engine
|
||||
// for the EngineCommandManager; namely, the underlying WebSocket
|
||||
@ -135,22 +178,38 @@ class EngineConnection {
|
||||
|
||||
set state(next: EngineConnectionState) {
|
||||
console.log(`${JSON.stringify(this.state)} → ${JSON.stringify(next)}`)
|
||||
if (next.type === EngineConnectionStateType.Disconnected) {
|
||||
|
||||
if (next.type === EngineConnectionStateType.Disconnecting) {
|
||||
console.trace()
|
||||
const sub = next.value
|
||||
if (sub.type === DisconnectedType.Error) {
|
||||
if (sub.type === DisconnectingType.Error) {
|
||||
// Record the last step we failed at.
|
||||
// (Check the current state that we're about to override that
|
||||
// it was a Connecting state.)
|
||||
console.log(sub)
|
||||
if (this._state.type === EngineConnectionStateType.Connecting) {
|
||||
if (!sub.value) sub.value = {}
|
||||
sub.value.lastConnectingValue = this._state.value
|
||||
}
|
||||
|
||||
console.error(sub.value)
|
||||
}
|
||||
}
|
||||
this._state = next
|
||||
this.onConnectionStateChange(this._state)
|
||||
}
|
||||
|
||||
private failedConnTimeout: Timeout | null
|
||||
|
||||
readonly url: string
|
||||
private readonly token?: string
|
||||
private onWebsocketOpen: (engineConnection: EngineConnection) => void
|
||||
private onDataChannelOpen: (engineConnection: EngineConnection) => void
|
||||
|
||||
// For now, this is only used by the NetworkHealthIndicator.
|
||||
// We can eventually use it for more, but one step at a time.
|
||||
private onConnectionStateChange: (state: EngineConnectionState) => void
|
||||
|
||||
// These are used for the EngineCommandManager and were created
|
||||
// before onConnectionStateChange existed.
|
||||
private onEngineConnectionOpen: (engineConnection: EngineConnection) => void
|
||||
private onConnectionStarted: (engineConnection: EngineConnection) => void
|
||||
private onClose: (engineConnection: EngineConnection) => void
|
||||
@ -162,17 +221,15 @@ class EngineConnection {
|
||||
constructor({
|
||||
url,
|
||||
token,
|
||||
onWebsocketOpen = () => {},
|
||||
onConnectionStateChange = () => {},
|
||||
onNewTrack = () => {},
|
||||
onEngineConnectionOpen = () => {},
|
||||
onConnectionStarted = () => {},
|
||||
onClose = () => {},
|
||||
onDataChannelOpen = () => {},
|
||||
}: {
|
||||
url: string
|
||||
token?: string
|
||||
onWebsocketOpen?: (engineConnection: EngineConnection) => void
|
||||
onDataChannelOpen?: (engineConnection: EngineConnection) => void
|
||||
onConnectionStateChange?: (state: EngineConnectionState) => void
|
||||
onEngineConnectionOpen?: (engineConnection: EngineConnection) => void
|
||||
onConnectionStarted?: (engineConnection: EngineConnection) => void
|
||||
onClose?: (engineConnection: EngineConnection) => void
|
||||
@ -181,8 +238,7 @@ class EngineConnection {
|
||||
this.url = url
|
||||
this.token = token
|
||||
this.failedConnTimeout = null
|
||||
this.onWebsocketOpen = onWebsocketOpen
|
||||
this.onDataChannelOpen = onDataChannelOpen
|
||||
this.onConnectionStateChange = onConnectionStateChange
|
||||
this.onEngineConnectionOpen = onEngineConnectionOpen
|
||||
this.onConnectionStarted = onConnectionStarted
|
||||
|
||||
@ -198,6 +254,7 @@ class EngineConnection {
|
||||
case EngineConnectionStateType.ConnectionEstablished:
|
||||
this.send({ type: 'ping' })
|
||||
break
|
||||
case EngineConnectionStateType.Disconnecting:
|
||||
case EngineConnectionStateType.Disconnected:
|
||||
clearInterval(pingInterval)
|
||||
break
|
||||
@ -209,17 +266,11 @@ class EngineConnection {
|
||||
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
||||
let connectRetryInterval = setInterval(() => {
|
||||
if (this.state.type !== EngineConnectionStateType.Disconnected) return
|
||||
switch (this.state.value.type) {
|
||||
case DisconnectedType.Error:
|
||||
|
||||
// Only try reconnecting when completely disconnected.
|
||||
clearInterval(connectRetryInterval)
|
||||
break
|
||||
case DisconnectedType.Timeout:
|
||||
console.log('Trying to reconnect')
|
||||
this.connect()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}, connectionTimeoutMs)
|
||||
}
|
||||
|
||||
@ -234,8 +285,8 @@ class EngineConnection {
|
||||
tearDown() {
|
||||
this.disconnectAll()
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Disconnected,
|
||||
value: { type: DisconnectedType.Quit },
|
||||
type: EngineConnectionStateType.Disconnecting,
|
||||
value: { type: DisconnectingType.Quit },
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,13 +403,15 @@ class EngineConnection {
|
||||
case 'failed':
|
||||
this.disconnectAll()
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Disconnected,
|
||||
type: EngineConnectionStateType.Disconnecting,
|
||||
value: {
|
||||
type: DisconnectedType.Error,
|
||||
value: new Error(
|
||||
type: DisconnectingType.Error,
|
||||
value: {
|
||||
error: new Error(
|
||||
'failed to negotiate ice connection; restarting'
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
break
|
||||
default:
|
||||
@ -473,8 +526,6 @@ class EngineConnection {
|
||||
dataChannelSpan.resolve?.()
|
||||
}
|
||||
|
||||
this.onDataChannelOpen(this)
|
||||
|
||||
// Everything is now connected.
|
||||
this.state = { type: EngineConnectionStateType.ConnectionEstablished }
|
||||
|
||||
@ -482,27 +533,20 @@ class EngineConnection {
|
||||
})
|
||||
|
||||
this.unreliableDataChannel.addEventListener('close', (event) => {
|
||||
console.log(event)
|
||||
console.log('unreliable data channel closed')
|
||||
this.disconnectAll()
|
||||
this.unreliableDataChannel = undefined
|
||||
|
||||
if (this.areAllConnectionsClosed()) {
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Disconnected,
|
||||
value: { type: DisconnectedType.Quit },
|
||||
}
|
||||
}
|
||||
this.finalizeIfAllConnectionsClosed()
|
||||
})
|
||||
|
||||
this.unreliableDataChannel.addEventListener('error', (event) => {
|
||||
this.disconnectAll()
|
||||
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Disconnected,
|
||||
type: EngineConnectionStateType.Disconnecting,
|
||||
value: {
|
||||
type: DisconnectedType.Error,
|
||||
value: new Error(event.toString()),
|
||||
type: DisconnectingType.Error,
|
||||
value: {
|
||||
error: new Error(event.toString()),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
@ -527,8 +571,6 @@ class EngineConnection {
|
||||
},
|
||||
}
|
||||
|
||||
this.onWebsocketOpen(this)
|
||||
|
||||
// This is required for when KCMA is running stand-alone / within Tauri.
|
||||
// Otherwise when run in a browser, the token is sent implicitly via
|
||||
// the Cookie header.
|
||||
@ -560,24 +602,19 @@ class EngineConnection {
|
||||
|
||||
this.websocket.addEventListener('close', (event) => {
|
||||
this.disconnectAll()
|
||||
this.websocket = undefined
|
||||
|
||||
if (this.areAllConnectionsClosed()) {
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Disconnected,
|
||||
value: { type: DisconnectedType.Quit },
|
||||
}
|
||||
}
|
||||
this.finalizeIfAllConnectionsClosed()
|
||||
})
|
||||
|
||||
this.websocket.addEventListener('error', (event) => {
|
||||
this.disconnectAll()
|
||||
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Disconnected,
|
||||
type: EngineConnectionStateType.Disconnecting,
|
||||
value: {
|
||||
type: DisconnectedType.Error,
|
||||
value: new Error(event.toString()),
|
||||
type: DisconnectingType.Error,
|
||||
value: {
|
||||
error: new Error(event.toString()),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
@ -706,10 +743,12 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
||||
// The local description is invalid, so there's no point continuing.
|
||||
this.disconnectAll()
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Disconnected,
|
||||
type: EngineConnectionStateType.Disconnecting,
|
||||
value: {
|
||||
type: DisconnectedType.Error,
|
||||
value: error,
|
||||
type: DisconnectingType.Error,
|
||||
value: {
|
||||
error,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
@ -787,13 +826,14 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
||||
return
|
||||
}
|
||||
this.failedConnTimeout = null
|
||||
this.disconnectAll()
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Disconnected,
|
||||
type: EngineConnectionStateType.Disconnecting,
|
||||
value: {
|
||||
type: DisconnectedType.Timeout,
|
||||
type: DisconnectingType.Timeout,
|
||||
},
|
||||
}
|
||||
this.disconnectAll()
|
||||
this.finalizeIfAllConnectionsClosed()
|
||||
}, connectionTimeoutMs)
|
||||
|
||||
this.onConnectionStarted(this)
|
||||
@ -816,11 +856,18 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
||||
this.websocket?.close()
|
||||
this.unreliableDataChannel?.close()
|
||||
this.pc?.close()
|
||||
|
||||
this.webrtcStatsCollector = undefined
|
||||
}
|
||||
areAllConnectionsClosed() {
|
||||
finalizeIfAllConnectionsClosed() {
|
||||
console.log(this.websocket, this.pc, this.unreliableDataChannel)
|
||||
return !this.websocket && !this.pc && !this.unreliableDataChannel
|
||||
const allClosed =
|
||||
this.websocket?.readyState === 3 &&
|
||||
this.pc?.connectionState === 'closed' &&
|
||||
this.unreliableDataChannel?.readyState === 'closed'
|
||||
if (allClosed) {
|
||||
this.state = { type: EngineConnectionStateType.Disconnected }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -893,6 +940,9 @@ export class EngineCommandManager {
|
||||
}
|
||||
} = {} as any
|
||||
|
||||
callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] =
|
||||
[]
|
||||
|
||||
constructor() {
|
||||
this.engineConnection = undefined
|
||||
;(async () => {
|
||||
@ -934,6 +984,11 @@ export class EngineCommandManager {
|
||||
this.engineConnection = new EngineConnection({
|
||||
url,
|
||||
token,
|
||||
onConnectionStateChange: (state: EngineConnectionState) => {
|
||||
for (let cb of this.callbacksEngineStateConnection) {
|
||||
cb(state)
|
||||
}
|
||||
},
|
||||
onEngineConnectionOpen: () => {
|
||||
this.resolveReady()
|
||||
setIsStreamReady(true)
|
||||
@ -1189,6 +1244,9 @@ export class EngineCommandManager {
|
||||
) {
|
||||
delete this.unreliableSubscriptions[event][id]
|
||||
}
|
||||
onConnectionStateChange(callback: (state: EngineConnectionState) => void) {
|
||||
this.callbacksEngineStateConnection.push(callback)
|
||||
}
|
||||
endSession() {
|
||||
// TODO: instead of sending a single command with `object_ids: Object.keys(this.artifactMap)`
|
||||
// we need to loop over them each individually because if the engine doesn't recognise a single
|
||||
|
Reference in New Issue
Block a user