Compare commits

...

37 Commits

Author SHA1 Message Date
b090f9959b fix macos 2024-06-03 11:49:46 +10:00
37f0f7b568 refactor test utils into test-utils 2024-06-03 09:56:18 +10:00
08c1745be0 fmt 2024-05-31 12:25:47 -04:00
a157dfe6d7 fmt 2024-05-31 12:22:19 -04:00
80b6688d04 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-05-31 16:12:59 +00:00
b7b87a38c4 fmt 2024-05-31 12:10:11 -04:00
ab56a165a4 Bump to 2 workers 2024-05-31 12:06:18 -04:00
9bcc307f32 Fix flakiness in for segments tests 2024-05-31 10:17:59 -04:00
5a3c6a3858 Fix tests unrelated to this PR 2024-05-31 10:16:21 -04:00
280f08945c ci again 2024-05-31 10:15:17 -04:00
e9fb3f7256 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-05-31 10:15:15 -04:00
56f0c43e4e Correct a vitest 2024-05-31 10:14:49 -04:00
4f9e8cbe15 Please the Playwright. Praise the Playwright. 2024-05-31 10:14:49 -04:00
0f21ca90c8 Add new instructions on running Playwright anywhere 2024-05-31 10:14:49 -04:00
c0bed02d72 Remove unused variables 2024-05-31 10:14:49 -04:00
3ab33e3810 run ci pls again 2024-05-31 10:14:49 -04:00
27ba89f867 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-05-31 10:14:49 -04:00
c52e4dcbe6 run ci pls 2024-05-31 10:14:49 -04:00
791c0487ae run ci pls 2024-05-31 10:14:49 -04:00
bd9c02fde9 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-05-31 10:14:49 -04:00
cda70687f4 Incorporate ping health; yarn make:dev; faster video stream loss notice 2024-05-31 10:14:49 -04:00
a7d785ab88 Fix typing 2024-05-31 10:14:49 -04:00
16a64b55db fmt 2024-05-31 10:14:49 -04:00
343ed04f7d wip 2024-05-31 10:14:49 -04:00
66ddce1348 wip 2024-05-31 10:14:49 -04:00
fade3b3995 Fix tsc 2024-05-31 10:14:49 -04:00
8e7465a823 Revert awaiting on lsp failure 2024-05-31 10:14:49 -04:00
05521d3ef3 Fix up types 2024-05-31 10:14:49 -04:00
37df4c6fc9 Fix formatting 2024-05-31 10:14:49 -04:00
db08e67215 Show when authentication is bad (cookie header only) 2024-05-31 10:14:49 -04:00
32f2411394 Catch LSP errors on bad auth 2024-05-31 10:14:49 -04:00
cc0377bb00 Don't do any stream events if network is not ok 2024-05-31 10:14:49 -04:00
ff08cef9f8 Refactor to use Context API for network status 2024-05-31 10:14:49 -04:00
39a6d265f2 Remove unused variable 2024-05-31 10:14:49 -04:00
545e89f7aa Add new error states to network status notification 2024-05-31 10:14:49 -04:00
d6ae23d881 Fix build errors 2024-05-31 10:14:49 -04:00
25b599d3e7 Reapply "Add ping pong health, remove a timeout interval, fix up netwo… (#1771)
This reverts commit 1913519f68.
2024-05-31 10:14:49 -04:00
32 changed files with 1384 additions and 960 deletions

14
Makefile Normal file
View File

@ -0,0 +1,14 @@
.PHONY: dev
WASM_LIB_FILES := $(wildcard src/wasm-lib/**/*.rs)
dev: node_modules public/wasm_lib_bg.wasm
yarn start
public/wasm_lib_bg.wasm: $(WASM_LIB_FILES)
yarn build:wasm-dev
node_modules: package.json
package.json:
yarn install

View File

@ -197,28 +197,32 @@ For more information on fuzzing you can check out
### Playwright
First time running plawright locally, you'll need to add the secrets file
For a portable way to run Playwright you'll need Docker.
After that, open a terminal and run:
```bash
touch ./e2e/playwright/playwright-secrets.env
printf 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets.env
docker run --network host --rm --init -it playwright/chrome:playwright-1.43.1
```
and in another terminal, run:
```bash
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn playwright test --project="Google Chrome" <test suite>
```
An example of a `<test suite>` is: `e2e/playwright/flow-tests.spec.ts`
YOU WILL NEED A PLAYWRIGHT-SECRETS.ENV FILE:
```bash
# ./e2e/playwright/playwright-secrets.env
token=<your-token>
snapshottoken=<your-snapshot-token>
```
then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens
then:
run playwright
```
yarn playwright test
```
run a specific test suite
```
yarn playwright test src/e2e-tests/example.spec.ts
```
run a specific test change the test from `test('...` to `test.only('...`
(note if you commit this, the tests will instantly fail without running any of the tests)

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -102,6 +102,29 @@ export async function getUtils(page: Page) {
const cdpSession =
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
let click00rCenter = { x: 0, y: 0 }
const click00 = (x: number, y: number) =>
page.mouse.click(click00rCenter.x + x, click00rCenter.y + y)
let click00rLastPos = { x: 0, y: 0 }
// The way we truncate is kinda odd apparently, so we need this function
// "[k]itty[c]ad round"
const kcRound = (n: number) => Math.trunc(n * 100) / 100
// To translate between screen and engine ("[U]nit") coordinates
// NOTE: these pretty much can't be perfect because of screen scaling.
// Handle on a case-by-case.
const toU = (x: number, y: number) => [
kcRound(x * 0.0854),
kcRound(-y * 0.0854), // Y is inverted in our coordinate system
]
// Turn the array into a string with specific formatting
const fromUToString = (xy: number[]) => `[${xy[0]}, ${xy[1]}]`
// Combine because used often
const toSU = (xy: number[]) => fromUToString(toU(xy[0], xy[1]))
return {
waitForAuthSkipAppStart: () => waitForPageLoad(page),
removeCurrentCode: () => removeCurrentCode(page),
@ -145,11 +168,15 @@ export async function getUtils(page: Page) {
y: bbox.y - angleYOffset,
}
},
getAngle: async (locator: string) => {
const overlay = page.locator(locator)
return Number(await overlay.getAttribute('data-overlay-angle'))
},
getBoundingBox: async (locator: string) =>
page
.locator(locator)
.boundingBox()
.then((box) => ({ x: box?.x || 0, y: box?.y || 0 })),
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
doAndWaitForCmd: async (
fn: () => Promise<void>,
commandType: string,
@ -217,6 +244,51 @@ export async function getUtils(page: Page) {
cdpSession?.send('Network.emulateNetworkConditions', networkOptions)
},
expectCodeToBe: async (str: string) => {
await expect(page.locator('.cm-content')).toHaveText(str)
await page.waitForTimeout(100)
},
click00rSetCenter: (x: number, y: number) => {
click00rCenter = { x, y }
},
click00r: (x?: number, y?: number) => {
// reset relative coordinates when anything is undefined
if (x === undefined || y === undefined) {
click00rLastPos.x = 0
click00rLastPos.y = 0
return
}
const ret = click00(click00rLastPos.x + x, click00rLastPos.y + y)
click00rLastPos.x += x
click00rLastPos.y += y
// Returns the new absolute coordinate if you need it.
return ret.then(() => [click00rLastPos.x, click00rLastPos.y])
},
toSU,
wiggleMove: async (
x: number,
y: number,
steps: number,
dist: number,
ang: number,
amplitude: number,
freq: number
) => {
const tau = Math.PI * 2
const deg = tau / 360
const step = dist / steps
for (let i = 0, j = 0; i < dist; i += step, j += 1) {
const y1 = Math.sin((tau / steps) * j * freq) * amplitude
const [x2, y2] = [
Math.cos(-ang * deg) * i - Math.sin(-ang * deg) * y1,
Math.sin(-ang * deg) * i + Math.cos(-ang * deg) * y1,
]
const [xr, yr] = [x2, y2]
await page.mouse.move(x + xr, y + yr, { steps: 2 })
}
},
}
}

View File

@ -10,7 +10,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.63",
"@kittycad/lib": "^0.0.64",
"@lezer/javascript": "^1.4.9",
"@open-rpc/client-js": "^1.8.1",
"@react-hook/resize-observer": "^2.0.1",
@ -95,7 +95,8 @@
"lint": "eslint --fix src",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
"postinstall": "yarn xstate:typegen",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
"make:dev": "make dev"
},
"prettier": {
"trailingComma": "es5",

View File

@ -18,7 +18,7 @@ export default defineConfig({
/* Retry on CI only */
retries: process.env.CI ? 3 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : 1,
workers: process.env.CI ? 2 : 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
@ -72,7 +72,7 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
command: 'yarn serve',
command: 'yarn start',
// url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
},

View File

@ -12,6 +12,8 @@ import SignIn from './routes/SignIn'
import { Auth } from './Auth'
import { isTauri } from './lib/isTauri'
import Home from './routes/Home'
import { NetworkContext } from './hooks/useNetworkContext'
import { useNetworkStatus } from './hooks/useNetworkStatus'
import makeUrlPathRelative from './lib/makeUrlPathRelative'
import DownloadAppBanner from 'components/DownloadAppBanner'
import { WasmErrBanner } from 'components/WasmErrBanner'
@ -155,5 +157,11 @@ const router = createBrowserRouter([
* @returns RouterProvider
*/
export const Router = () => {
return <RouterProvider router={router} />
const networkStatus = useNetworkStatus()
return (
<NetworkContext.Provider value={networkStatus as any}>
<RouterProvider router={router} />
</NetworkContext.Provider>
)
}

View File

@ -3,13 +3,11 @@ import { isCursorInSketchCommandRange } from 'lang/util'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { useModelingContext } from 'hooks/useModelingContext'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ActionButton } from 'components/ActionButton'
import { isSingleCursorInPipe } from 'lang/queryAst'
import { useKclContext } from 'lang/KclProvider'
import {
NetworkHealthState,
useNetworkStatus,
} from 'components/NetworkHealthIndicator'
import { useStore } from 'useStore'
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
import { useHotkeys } from 'react-hotkeys-hook'
@ -38,14 +36,16 @@ export function Toolbar({
}, [engineCommandManager.artifactMap, context.selectionRanges])
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
const { overallState } = useNetworkStatus()
const { overallState } = useNetworkContext()
const { isExecuting } = useKclContext()
const { isStreamReady } = useStore((s) => ({
isStreamReady: s.isStreamReady,
}))
const disableAllButtons =
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
(overallState !== NetworkHealthState.Ok &&
overallState !== NetworkHealthState.Weak) ||
isExecuting ||
!isStreamReady
useHotkeys(
'l',

View File

@ -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>
)

View File

@ -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.

View File

@ -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]
)

View File

@ -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
}
/>

View File

@ -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>
)}

View File

@ -474,19 +474,13 @@ const completionRequester = (client: LanguageServerClient) => {
}
export const copilotPlugin = (options: LanguageServerOptions): Extension => {
let plugin: LanguageServerPlugin | null = null
return [
documentUri.of(options.documentUri),
languageId.of('kcl'),
workspaceFolders.of(options.workspaceFolders),
ViewPlugin.define(
(view) =>
(plugin = new LanguageServerPlugin(
options.client,
view,
options.allowHTMLContent
))
new LanguageServerPlugin(options.client, view, options.allowHTMLContent)
),
completionDecoration,
Prec.highest(completionPlugin(options.client)),

View File

@ -0,0 +1,25 @@
import { createContext, useContext } from 'react'
import {
ConnectingTypeGroup,
initialConnectingTypeGroupState,
} from '../lang/std/engineConnection'
import { NetworkHealthState } from './useNetworkStatus'
export const NetworkContext = createContext({
hasIssues: undefined,
overallState: NetworkHealthState.Disconnected,
internetConnected: true,
steps: structuredClone(initialConnectingTypeGroupState),
issues: {
[ConnectingTypeGroup.WebSocket]: undefined,
[ConnectingTypeGroup.ICE]: undefined,
[ConnectingTypeGroup.WebRTC]: undefined,
},
error: undefined,
setHasCopied: (b: boolean) => {},
hasCopied: false,
pingPongHealth: undefined,
})
export const useNetworkContext = () => {
return useContext(NetworkContext)
}

View File

@ -0,0 +1,213 @@
import { useEffect, useState } from 'react'
import {
ConnectingType,
ConnectingTypeGroup,
DisconnectingType,
EngineCommandManagerEvents,
EngineConnectionEvents,
EngineConnectionStateType,
ErrorType,
initialConnectingTypeGroupState,
} from '../lang/std/engineConnection'
import { engineCommandManager } from '../lib/singletons'
export enum NetworkHealthState {
Ok,
Weak,
Issue,
Disconnected,
}
// Must be called from one place in the application.
// We've chosen the <Router /> component for this.
export function useNetworkStatus() {
const [steps, setSteps] = useState(
structuredClone(initialConnectingTypeGroupState)
)
const [internetConnected, setInternetConnected] = useState<boolean>(true)
const [overallState, setOverallState] = useState<NetworkHealthState>(
NetworkHealthState.Disconnected
)
const [pingPongHealth, setPingPongHealth] = useState<
undefined | 'OK' | 'TIMEOUT'
>(undefined)
const [hasCopied, setHasCopied] = useState<boolean>(false)
const [error, setError] = useState<ErrorType | undefined>(undefined)
const hasIssue = (i: [ConnectingType, boolean | undefined]) =>
i[1] === undefined ? i[1] : !i[1]
const [issues, setIssues] = useState<
Record<ConnectingTypeGroup, boolean | undefined>
>({
[ConnectingTypeGroup.WebSocket]: undefined,
[ConnectingTypeGroup.ICE]: undefined,
[ConnectingTypeGroup.WebRTC]: undefined,
})
const [hasIssues, setHasIssues] = useState<boolean | undefined>(undefined)
useEffect(() => {
setOverallState(
!internetConnected
? NetworkHealthState.Disconnected
: hasIssues || hasIssues === undefined
? NetworkHealthState.Issue
: pingPongHealth === 'TIMEOUT'
? NetworkHealthState.Weak
: NetworkHealthState.Ok
)
}, [hasIssues, internetConnected, pingPongHealth])
useEffect(() => {
const onlineCallback = () => {
setInternetConnected(true)
}
const offlineCallback = () => {
setInternetConnected(false)
setSteps(structuredClone(initialConnectingTypeGroupState))
}
window.addEventListener('online', onlineCallback)
window.addEventListener('offline', offlineCallback)
return () => {
window.removeEventListener('online', onlineCallback)
window.removeEventListener('offline', offlineCallback)
}
}, [])
useEffect(() => {
const issues = {
[ConnectingTypeGroup.WebSocket]: steps[
ConnectingTypeGroup.WebSocket
].reduce(
(acc: boolean | undefined, a) =>
acc === true || acc === undefined ? acc : hasIssue(a),
false
),
[ConnectingTypeGroup.ICE]: steps[ConnectingTypeGroup.ICE].reduce(
(acc: boolean | undefined, a) =>
acc === true || acc === undefined ? acc : hasIssue(a),
false
),
[ConnectingTypeGroup.WebRTC]: steps[ConnectingTypeGroup.WebRTC].reduce(
(acc: boolean | undefined, a) =>
acc === true || acc === undefined ? acc : hasIssue(a),
false
),
}
setIssues(issues)
}, [steps])
useEffect(() => {
setHasIssues(
issues[ConnectingTypeGroup.WebSocket] ||
issues[ConnectingTypeGroup.ICE] ||
issues[ConnectingTypeGroup.WebRTC]
)
}, [issues])
useEffect(() => {
const onPingPongChange = ({ detail: state }: CustomEvent) => {
setPingPongHealth(state)
}
const onConnectionStateChange = ({
detail: engineConnectionState,
}: CustomEvent) => {
setSteps((steps) => {
let nextSteps = structuredClone(steps)
if (
engineConnectionState.type === EngineConnectionStateType.Connecting
) {
const groups = Object.values(nextSteps)
for (let group of groups) {
for (let step of group) {
if (step[0] !== engineConnectionState.value.type) continue
step[1] = true
}
}
}
if (
engineConnectionState.type === EngineConnectionStateType.Disconnecting
) {
const groups = Object.values(nextSteps)
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
}
}
}
if (engineConnectionState.value.type === DisconnectingType.Error) {
setError(engineConnectionState.value.value)
}
}
}
// Reset the state of all steps if we have disconnected.
if (
engineConnectionState.type === EngineConnectionStateType.Disconnected
) {
return structuredClone(initialConnectingTypeGroupState)
}
return nextSteps
})
}
const onEngineAvailable = ({ detail: engineConnection }: CustomEvent) => {
engineConnection.addEventListener(
EngineConnectionEvents.PingPongChanged,
onPingPongChange as EventListener
)
engineConnection.addEventListener(
EngineConnectionEvents.ConnectionStateChanged,
onConnectionStateChange as EventListener
)
}
engineCommandManager.addEventListener(
EngineCommandManagerEvents.EngineAvailable,
onEngineAvailable as EventListener
)
return () => {
engineCommandManager.removeEventListener(
EngineCommandManagerEvents.EngineAvailable,
onEngineAvailable as EventListener
)
// When the component is unmounted these should be assigned, but it's possible
// the component mounts and unmounts before engine is available.
engineCommandManager.engineConnection?.addEventListener(
EngineConnectionEvents.PingPongChanged,
onPingPongChange as EventListener
)
engineCommandManager.engineConnection?.addEventListener(
EngineConnectionEvents.ConnectionStateChanged,
onConnectionStateChange as EventListener
)
}
}, [])
return {
hasIssues,
overallState,
internetConnected,
steps,
issues,
error,
setHasCopied,
hasCopied,
pingPongHealth,
}
}

View File

@ -43,7 +43,7 @@ export function useSetupEngineManager(
engineCommandManager.pool = settings.pool
}
useLayoutEffect(() => {
const startEngineInstance = () => {
// Load the engine command manager once with the initial width and height,
// then we do not want to reload it.
const { width: quadWidth, height: quadHeight } = getDimensions(
@ -73,7 +73,12 @@ export function useSetupEngineManager(
})
hasSetNonZeroDimensions.current = true
}
}, [streamRef?.current?.offsetWidth, streamRef?.current?.offsetHeight])
}
useLayoutEffect(startEngineInstance, [
streamRef?.current?.offsetWidth,
streamRef?.current?.offsetHeight,
])
useEffect(() => {
const handleResize = deferExecution(() => {
@ -96,8 +101,20 @@ export function useSetupEngineManager(
}
}, 500)
const onOnline = () => {
startEngineInstance()
}
const onOffline = () => {
engineCommandManager.tearDown()
}
window.addEventListener('online', onOnline)
window.addEventListener('offline', onOffline)
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('online', onOnline)
window.removeEventListener('offline', onOffline)
window.removeEventListener('resize', handleResize)
}
}, [])

View File

@ -7,12 +7,10 @@ import { authMachine } from 'machines/authMachine'
import { settingsMachine } from 'machines/settingsMachine'
import { homeMachine } from 'machines/homeMachine'
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
import {
NetworkHealthState,
useNetworkStatus,
} from 'components/NetworkHealthIndicator'
import { useKclContext } from 'lang/KclProvider'
import { useStore } from 'useStore'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
// This might not be necessary, AnyStateMachine from xstate is working
export type AllMachines =
@ -47,7 +45,7 @@ export default function useStateMachineCommands<
onCancel,
}: UseStateMachineCommandsArgs<T, S>) {
const { commandBarSend } = useCommandsContext()
const { overallState } = useNetworkStatus()
const { overallState } = useNetworkContext()
const { isExecuting } = useKclContext()
const { isStreamReady } = useStore((s) => ({
isStreamReady: s.isStreamReady,
@ -55,7 +53,10 @@ export default function useStateMachineCommands<
useEffect(() => {
const disableAllButtons =
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
(overallState !== NetworkHealthState.Ok &&
overallState !== NetworkHealthState.Weak) ||
isExecuting ||
!isStreamReady
const newCommands = state.nextEvents
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))

File diff suppressed because it is too large Load Diff

View File

@ -318,7 +318,6 @@ function resetAndSetEngineEntitySelectionCmds(
selections: SelectionToEngine[]
): Models['WebSocketRequest_type'][] {
if (!engineCommandManager.engineConnection?.isReady()) {
console.log('engine connection is not ready')
return []
}
return [

View File

@ -1880,10 +1880,10 @@
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
"@kittycad/lib@^0.0.63":
version "0.0.63"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.63.tgz#cc70cf1c0780543bbca6f55aae40d0904cfd45d7"
integrity sha512-fDpGnycumT1xI/tSubRZzU9809/7s+m06w2EuJzxowgFrdIlvThnIHVf3EYvSujdFb0bHR/LZjodAw2ocXkXZw==
"@kittycad/lib@^0.0.64":
version "0.0.64"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.64.tgz#0cea0788cd8af4a8964ddbf7152028affadcb17f"
integrity sha512-qHyvNYKbhsfR5aXLFrdKrBQ4JI+0G0v096oROD3HatJ+AIzg5H0THmI+rMnQ9L4zx4U6n1A9gLi7ZQjSsZsleg==
dependencies:
node-fetch "3.3.2"
openapi-types "^12.0.0"
@ -8234,16 +8234,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -8316,14 +8307,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -9305,7 +9289,7 @@ workerpool@6.2.1:
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -9323,15 +9307,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"