Skip to content

Commit

Permalink
add redux for handling data-layer ws (re)connection (#644)
Browse files Browse the repository at this point in the history
* add redux for handling data-layer ws connection and error handling

* fix yield calls

* add warning messages

* remove pointer events when disconnected

* add tests

* remove dataLayer from sdk context

* fix tests

* fix leak of memory
  • Loading branch information
gonpombo8 authored Jun 20, 2023
1 parent f9d6de9 commit 39ade45
Show file tree
Hide file tree
Showing 59 changed files with 1,188 additions and 269 deletions.
462 changes: 462 additions & 0 deletions packages/@dcl/inspector/package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions packages/@dcl/inspector/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
"@dcl/ecs-math": "2.0.2",
"@dcl/rpc": "^1.1.1",
"@dcl/schemas": "^6.11.1",
"@reduxjs/toolkit": "^1.9.5",
"@testing-library/react": "^14.0.0",
"@types/node": "^18.11.18",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/react-modal": "^3.16.0",
"@types/redux-saga": "^0.10.5",
"@vscode/webview-ui-toolkit": "^1.2.2",
"@well-known-components/pushable-channel": "^1.0.3",
"classnames": "^2.3.2",
Expand All @@ -31,7 +33,10 @@
"react-dom": "^18.2.0",
"react-icons": "^4.7.1",
"react-modal": "^3.16.1",
"react-redux": "^8.1.0",
"react-resizable-panels": "^0.0.48",
"redux-saga": "^1.2.3",
"redux-saga-test-plan": "^4.0.6",
"typescript": "^5.0.2"
},
"files": [
Expand Down
6 changes: 4 additions & 2 deletions packages/@dcl/inspector/src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import './App.css'
import Assets from '../Assets'
import { useSelectedEntity } from '../../hooks/sdk/useSelectedEntity'
import { useWindowSize } from '../../hooks/useWindowSize'
import { useAppSelector } from '../../redux/hooks'
import { getError } from '../../redux/data-layer'

const App = () => {
const selectedEntity = useSelectedEntity()
Expand All @@ -19,9 +21,9 @@ const App = () => {

// Footer's height is 48 pixels, so we need to calculate the percentage of the screen that it takes to pass as the minSize prop for the Panel
const footerMin = (48 / height!) * 100

const disconnected = useAppSelector(getError)
return (
<div className="App">
<div className="App" style={{ pointerEvents: disconnected ? 'none' : 'auto' }}>
<PanelGroup direction="vertical" autoSaveId="vertical">
<Panel>
<PanelGroup direction="horizontal" autoSaveId="horizontal">
Expand Down
39 changes: 18 additions & 21 deletions packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { RxCross2 } from 'react-icons/rx'
import { IoIosImage } from 'react-icons/io'

import FileInput from '../FileInput'
import { withSdk } from '../../hoc/withSdk'
import { Container } from '../Container'
import { TextField } from '../EntityInspector/TextField'
import { Block } from '../Block'
Expand All @@ -16,6 +15,8 @@ import { GLTFValidation } from '@babylonjs/loaders'
import './ImportAsset.css'
import classNames from 'classnames'
import { withAssetDir } from '../../lib/data-layer/host/fs-utils'
import { getDataLayer } from '../../redux/data-layer'
import { useAppSelector } from '../../redux/hooks'

const ONE_MB_IN_BYTES = 1_048_576
const ONE_GB_IN_BYTES = ONE_MB_IN_BYTES * 1024
Expand All @@ -29,9 +30,9 @@ type ValidationError = string | null
async function validateGltf(data: ArrayBuffer): Promise<ValidationError> {
let result
try {
result = await GLTFValidation.ValidateAsync(
data, '', '', (uri) => { throw new Error('external references are not supported yet')}
)
result = await GLTFValidation.ValidateAsync(data, '', '', (uri) => {
throw new Error('external references are not supported yet')
})
} catch (error) {
return `Invalid GLTF: ${error}`
}
Expand All @@ -42,13 +43,13 @@ async function validateGltf(data: ArrayBuffer): Promise<ValidationError> {
Babylon's type declarations incorrectly state that result.issues.messages
is an Array<string>. In fact, it's an array of objects with useful properties.
*/
type BabylonValidationIssue = {severity: number, code: string}
type BabylonValidationIssue = { severity: number; code: string }
const severity = (issue as unknown as BabylonValidationIssue).severity
/*
Severity codes are Error (0), Warning (1), Information (2), Hint (3).
https://github.com/KhronosGroup/glTF-Validator/blob/main/lib/src/errors.dart
*/
if (severity == 0) {
if (severity === 0) {
const message = (issue as unknown as BabylonValidationIssue).code
return `Invalid GLTF: ${message}`
}
Expand All @@ -59,8 +60,10 @@ async function validateGltf(data: ArrayBuffer): Promise<ValidationError> {
}
}

const ImportAsset = withSdk<PropTypes>(({ sdk, onSave }) => {
const ImportAsset: React.FC<PropTypes> = ({ onSave }) => {
// TODO: multiple files
const dataLayer = useAppSelector(getDataLayer)

const [file, setFile] = useState<File>()
const [validationError, setValidationError] = useState<ValidationError>(null)
const [assetName, setAssetName] = useState<string>('')
Expand Down Expand Up @@ -92,17 +95,17 @@ const ImportAsset = withSdk<PropTypes>(({ sdk, onSave }) => {
}

const gltfValidationError = await validateGltf(binary)
if (gltfValidationError != null) {
if (gltfValidationError !== null) {
setValidationError(gltfValidationError)
return
}

const content: Map<string, Uint8Array> = new Map()
content.set(assetName + '.' + assetExtension, new Uint8Array(binary))

const basePath = withAssetDir((await sdk!.dataLayer.getProjectData({})).path)
const basePath = withAssetDir((await dataLayer!.getProjectData({})).path)

await sdk!.dataLayer.importAsset({
await dataLayer?.importAsset({
content,
basePath,
assetPackageName: ''
Expand All @@ -124,10 +127,8 @@ const ImportAsset = withSdk<PropTypes>(({ sdk, onSave }) => {

const invalidName = !!assets.find((asset) => {
const [packageName, otherAssetName] = removeBasePath(basePath, asset.path).split('/')
if (packageName === 'builder')
return false
else
return otherAssetName?.toLocaleLowerCase() === (assetName?.toLocaleLowerCase() + '.' + assetExtension)
if (packageName === 'builder') return false
else return otherAssetName?.toLocaleLowerCase() === assetName?.toLocaleLowerCase() + '.' + assetExtension
})

return (
Expand Down Expand Up @@ -155,22 +156,18 @@ const ImportAsset = withSdk<PropTypes>(({ sdk, onSave }) => {
</Container>
<div className={classNames({ error: !!invalidName })}>
<Block label="Asset name">
<TextField
label=""
value={assetName}
onChange={handleNameChange}
/>
<TextField label="" value={assetName} onChange={handleNameChange} />
</Block>
<Button disabled={invalidName || !!validationError} onClick={handleSave}>
Import
</Button>
<span className='error'>{validationError}</span>
<span className="error">{validationError}</span>
</div>
</div>
)}
</FileInput>
</div>
)
})
}

export default ImportAsset
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import React from 'react'

import { useAssetTree } from '../../hooks/catalog/useAssetTree'
import { useFileSystem } from '../../hooks/catalog/useFileSystem'
import ProjectView from './ProjectView'
Expand All @@ -6,10 +8,12 @@ import { AssetNodeFolder } from './types'

import './ProjectAssetExplorer.css'

export function ProjectAssetExplorer() {
function ProjectAssetExplorer() {
const [files] = useFileSystem()
const { tree } = useAssetTree(files)
const folders = tree.children.filter((item) => item.type === 'folder') as AssetNodeFolder[]

return <ProjectView folders={folders} />
}

export default React.memo(ProjectAssetExplorer)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { AssetNode, AssetNodeFolder } from './types'
import { getFullNodePath } from './utils'
import Search from '../Search'
import { withAssetDir } from '../../lib/data-layer/host/fs-utils'
import { getDataLayer } from '../../redux/data-layer'
import { useAppSelector } from '../../redux/hooks'

function noop() {}

Expand All @@ -36,6 +38,8 @@ const FilesTree = Tree<string>()

function ProjectView({ folders }: Props) {
const sdk = useSdk()
const dataLayer = useAppSelector(getDataLayer)

const [open, setOpen] = useState(new Set<string>())
const [modal, setModal] = useState<ModalState | undefined>(undefined)
const [lastSelected, setLastSelected] = useState<string>()
Expand Down Expand Up @@ -146,12 +150,11 @@ function ProjectView({ folders }: Props) {

const removeAsset = useCallback(
async (path: string, _: Entity[] = []) => {
if (!sdk) return
const { dataLayer } = sdk
if (!dataLayer) return
await dataLayer.removeAsset({ path })
fileSystemEvent.emit('change')
},
[sdk]
[dataLayer]
)

const handleConfirm = useCallback(async () => {
Expand Down Expand Up @@ -251,7 +254,7 @@ function ProjectView({ folders }: Props) {
<Tile
valueId={lastSelected}
value={selectedTreeNode}
getDragContext= {handleDragContext}
getDragContext={handleDragContext}
onSelect={handleClickFolder(selectedTreeNode.name)}
onRemove={handleRemove}
dndType={DRAG_N_DROP_ASSET_KEY}
Expand All @@ -266,15 +269,18 @@ function ProjectView({ folders }: Props) {
function NodeIcon({ value }: { value?: TreeNode }) {
if (!value) return null
if (value.type === 'folder') {
return <div style={{ marginRight: '4px', marginLeft: '2px', marginTop: '2px' }}><FolderIcon /></div>
}
else
return (
<>
<svg style={{ width: '4px', height: '4px' }} />
<IoIosImage style={{ marginRight: '4px'}} />
</>
)
return (
<div style={{ marginRight: '4px', marginLeft: '2px', marginTop: '2px' }}>
<FolderIcon />
</div>
)
} else
return (
<>
<svg style={{ width: '4px', height: '4px' }} />
<IoIosImage style={{ marginRight: '4px' }} />
</>
)
}

export default React.memo(ProjectView)
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import { ProjectAssetExplorer } from './ProjectAssetExplorer'
import ProjectAssetExplorer from './ProjectAssetExplorer'
export { ProjectAssetExplorer }
10 changes: 7 additions & 3 deletions packages/@dcl/inspector/src/components/Renderer/Renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { Vector3 } from '@babylonjs/core'
import { Loader } from 'decentraland-ui/dist/components/Loader/Loader'
import { Dimmer } from 'decentraland-ui/dist/components/Dimmer/Dimmer'

import { withAssetDir } from '../../lib/data-layer/host/fs-utils'
import { useAppSelector } from '../../redux/hooks'
import { getDataLayer } from '../../redux/data-layer'
import { BuilderAsset, DROP_TYPES, IDrop, ProjectAssetDrop, isDropType } from '../../lib/sdk/drag-drop'
import { useRenderer } from '../../hooks/sdk/useRenderer'
import { useSdk } from '../../hooks/sdk/useSdk'
Expand All @@ -16,18 +19,19 @@ import { AssetNodeItem } from '../ProjectAssetExplorer/types'
import { IAsset } from '../AssetsCatalog/types'
import { getModel, isAsset } from '../EntityInspector/GltfInspector/utils'
import { useIsMounted } from '../../hooks/useIsMounted'
import { Warnings } from './Warnings'
import { Warnings } from '../Warnings'
import { CameraSpeed } from './CameraSpeed'

import './Renderer.css'
import { withAssetDir } from '../../lib/data-layer/host/fs-utils'

const fixedNumber = (val: number) => Math.round(val * 1e2) / 1e2

const Renderer: React.FC = () => {
const canvasRef = React.useRef<HTMLCanvasElement>(null)
useRenderer(() => canvasRef)
const sdk = useSdk()
const dataLayer = useAppSelector(getDataLayer)

const [isLoading, setIsLoading] = useState(false)
const isMounted = useIsMounted()
const [files, init] = useFileSystem()
Expand Down Expand Up @@ -84,7 +88,7 @@ const Renderer: React.FC = () => {
})
)

await sdk!.dataLayer.importAsset({
await dataLayer?.importAsset({
content: new Map(Object.entries(fileContent)),
basePath: withAssetDir(destFolder),
assetPackageName
Expand Down
14 changes: 9 additions & 5 deletions packages/@dcl/inspector/src/components/Toolbar/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import { withSdk } from '../../hoc/withSdk'
import { Gizmos } from './Gizmos'
import { Preferences } from './Preferences'
import { ToolbarButton } from './ToolbarButton'

import './Toolbar.css'
import { getDataLayer } from '../../redux/data-layer'
import { useAppSelector } from '../../redux/hooks'
import { DataLayerRpcClient } from '../../lib/data-layer/types'

const Toolbar = withSdk(({ sdk }) => {
const dataLayer = useAppSelector(getDataLayer)
const [save, isDirty] = useSave()
const handleInspector = useCallback(() => {
const { debugLayer } = sdk.scene
Expand All @@ -23,25 +26,26 @@ const Toolbar = withSdk(({ sdk }) => {
}, [])

const handleUndoRedo = useCallback(
(fn: typeof sdk.dataLayer.undo) => async () => {
(fn?: DataLayerRpcClient['undo']) => async () => {
if (!fn) return
const { type } = await fn({})
if (type === 'file') {
fileSystemEvent.emit('change')
}
saveEvent.emit('change', true)
},
[]
[dataLayer]
)

return (
<div className="Toolbar">
<ToolbarButton className="save" onClick={save} title={isDirty ? 'Save changes' : 'All changes saved'}>
{isDirty ? <BiSave /> : <BiBadgeCheck />}
</ToolbarButton>
<ToolbarButton className="undo" title='Undo' onClick={handleUndoRedo(sdk?.dataLayer.undo)}>
<ToolbarButton className="undo" title="Undo" onClick={handleUndoRedo(dataLayer?.undo)}>
<BiUndo />
</ToolbarButton>
<ToolbarButton className="redo" title='Redo' onClick={handleUndoRedo(sdk?.dataLayer.redo)}>
<ToolbarButton className="redo" title="Redo" onClick={handleUndoRedo(dataLayer?.redo)}>
<BiRedo />
</ToolbarButton>
<Gizmos />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react'
import { Warning } from '../Warning'
import { useGizmoAlignment } from '../../../../hooks/editor/useGizmoAlignment'
import { useSelectedEntity } from '../../../../hooks/sdk/useSelectedEntity'
import { ROOT } from '../../../../lib/sdk/tree'
import { withSdk } from '../../../../hoc/withSdk'
import { useComponentValue } from '../../../../hooks/sdk/useComponentValue'
import { GizmoType } from '../../../../lib/utils/gizmo'
import { useGizmoAlignment } from '../../../hooks/editor/useGizmoAlignment'
import { useSelectedEntity } from '../../../hooks/sdk/useSelectedEntity'
import { ROOT } from '../../../lib/sdk/tree'
import { withSdk } from '../../../hoc/withSdk'
import { useComponentValue } from '../../../hooks/sdk/useComponentValue'
import { GizmoType } from '../../../lib/utils/gizmo'

const RotationGizmoLocalAlignmentDisabled: React.FC = withSdk(({ sdk }) => {
const selectedEntity = useSelectedEntity()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react'

import { Warning } from '../Warning'
import { useAppSelector } from '../../../redux/hooks'
import { ErrorType, getError } from '../../../redux/data-layer'

const mapError = {
[ErrorType.Disconnected]: 'Socket disconnected. Please refresh the page',
[ErrorType.Reconnecting]: 'Disconnected. Trying to reconnect...'
}

const SocketConnection: React.FC = () => {
const error = useAppSelector(getError)
if (!error) return null
return <Warning title={mapError[error]} />
}

export default React.memo(SocketConnection)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import SocketConnection from './SocketConnection'
export { SocketConnection }
Loading

0 comments on commit 39ade45

Please sign in to comment.