Skip to content

Commit

Permalink
Optimize assets table (#10522)
Browse files Browse the repository at this point in the history
- Fix enso-org/cloud-v2#1379
- Stop re-rendering the entire `AssetsTable` on every `AssetEvent`
- This is achieved by using `zustand` for fine-grained reactivity.

Other misc fixes:
- Fix "unique key prop" error in sidebar
- Vertically center asset panel toggle icon in top bar
- Limit width of asset names (show tooltip when asset name is too wide)

# Important Notes
None
  • Loading branch information
somebody1234 authored Jul 15, 2024
1 parent bfb1d8e commit 1514381
Show file tree
Hide file tree
Showing 31 changed files with 493 additions and 367 deletions.
11 changes: 6 additions & 5 deletions app/ide-desktop/lib/common/src/utilities/data/newtype.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/** @file Emulates `newtype`s in TypeScript. */

/* eslint-disable @typescript-eslint/consistent-type-definitions */

// ===============
// === Newtype ===
// ===============

/** An interface specifying the variant of a newtype. */
export interface NewtypeVariant<TypeName extends string> {
type NewtypeVariant<TypeName extends string> = {
// eslint-disable-next-line @typescript-eslint/naming-convention
readonly _$type: TypeName
}
Expand All @@ -14,7 +16,7 @@ export interface NewtypeVariant<TypeName extends string> {
* This is safe, as the discriminator should be a string literal type anyway. */
// This is required for compatibility with the dependency `enso-chat`.
// eslint-disable-next-line no-restricted-syntax
export interface MutableNewtypeVariant<TypeName extends string> {
type MutableNewtypeVariant<TypeName extends string> = {
// eslint-disable-next-line @typescript-eslint/naming-convention
_$type: TypeName
}
Expand All @@ -36,15 +38,14 @@ export type Newtype<T, TypeName extends string> = NewtypeVariant<TypeName> & T

/** Extracts the original type out of a {@link Newtype}.
* Its only use is in {@link newtypeConstructor}. */
export type UnNewtype<T extends Newtype<unknown, string>> = T extends infer U &
NewtypeVariant<T['_$type']>
type UnNewtype<T extends Newtype<unknown, string>> = T extends infer U & NewtypeVariant<T['_$type']>
? U extends infer V & MutableNewtypeVariant<T['_$type']>
? V
: U
: NotNewtype & Omit<T, '_$type'>

/** An interface that matches a type if and only if it is not a newtype. */
export interface NotNewtype {
type NotNewtype = {
// eslint-disable-next-line @typescript-eslint/naming-convention
readonly _$type?: never
}
Expand Down
3 changes: 2 additions & 1 deletion app/ide-desktop/lib/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"tiny-invariant": "^1.3.3",
"ts-results": "^3.3.0",
"validator": "^13.12.0",
"zod": "^3.23.8"
"zod": "^3.23.8",
"zustand": "^4.5.4"
},
"devDependencies": {
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
Expand Down
14 changes: 12 additions & 2 deletions app/ide-desktop/lib/dashboard/src/components/EditableSpan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import * as eventModule from '#/utilities/event'
import * as sanitizedEventTargets from '#/utilities/sanitizedEventTargets'
import * as tailwindMerge from '#/utilities/tailwindMerge'

// =================
// === Constants ===
// =================

const WIDTH_CLASS_NAME = 'max-w-60'

// ====================
// === EditableSpan ===
// ====================
Expand Down Expand Up @@ -75,7 +81,7 @@ export default function EditableSpan(props: EditableSpanProps) {
if (editable) {
return (
<form
className="flex grow gap-1.5"
className={tailwindMerge.twMerge('flex grow gap-1.5', WIDTH_CLASS_NAME)}
onBlur={event => {
const currentTarget = event.currentTarget
if (!currentTarget.contains(event.relatedTarget)) {
Expand Down Expand Up @@ -162,7 +168,11 @@ export default function EditableSpan(props: EditableSpanProps) {
)
} else {
return (
<ariaComponents.Text data-testid={props['data-testid']} className={className}>
<ariaComponents.Text
data-testid={props['data-testid']}
truncate="1"
className={tailwindMerge.twMerge(WIDTH_CLASS_NAME, className)}
>
{children}
</ariaComponents.Text>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import BlankIcon from 'enso-assets/blank.svg'

import * as backendHooks from '#/hooks/backendHooks'
import * as dragAndDropHooks from '#/hooks/dragAndDropHooks'
import * as eventHooks from '#/hooks/eventHooks'
import * as setAssetHooks from '#/hooks/setAssetHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'

Expand All @@ -20,6 +19,7 @@ import type * as dashboard from '#/pages/dashboard/Dashboard'

import AssetContextMenu from '#/layouts/AssetContextMenu'
import type * as assetsTable from '#/layouts/AssetsTable'
import * as eventListProvider from '#/layouts/AssetsTable/EventListProvider'
import Category from '#/layouts/CategorySwitcher/Category'

import * as aria from '#/components/aria'
Expand Down Expand Up @@ -110,15 +110,17 @@ export default function AssetRow(props: AssetRowProps) {
} = props
const { setSelected, allowContextMenu, onContextMenu, state, columns, onClick } = props
const { grabKeyboardFocus, doOpenProject, doCloseProject } = props
const { backend, visibilities, assetEvents, dispatchAssetEvent, dispatchAssetListEvent } = state
const { nodeMap, setAssetPanelProps, doToggleDirectoryExpansion, doCopy, doCut, doPaste } = state
const { setIsAssetPanelTemporarilyVisible, scrollContainerRef, rootDirectoryId } = state
const { setIsAssetPanelTemporarilyVisible, scrollContainerRef, rootDirectoryId, backend } = state
const { visibilities } = state

const draggableProps = dragAndDropHooks.useDraggable()
const { user } = authProvider.useNonPartialUserSession()
const { setModal, unsetModal } = modalProvider.useSetModal()
const { getText } = textProvider.useText()
const toastAndLog = toastAndLogHooks.useToastAndLog()
const dispatchAssetEvent = eventListProvider.useDispatchAssetEvent()
const dispatchAssetListEvent = eventListProvider.useDispatchAssetListEvent()
const [isDraggedOver, setIsDraggedOver] = React.useState(false)
const [item, setItem] = React.useState(rawItem)
const rootRef = React.useRef<HTMLElement | null>(null)
Expand Down Expand Up @@ -434,7 +436,7 @@ export default function AssetRow(props: AssetRowProps) {
)
}, [setModal, asset.description, setAsset, backend, item.item.id, item.item.title])

eventHooks.useEventHandler(assetEvents, async event => {
eventListProvider.useAssetEventListener(async event => {
if (state.category === Category.trash) {
switch (event.type) {
case AssetEventType.deleteForever: {
Expand Down Expand Up @@ -696,7 +698,7 @@ export default function AssetRow(props: AssetRowProps) {
}
}
}
})
}, item.initialAssetEvents)

const clearDragState = React.useCallback(() => {
setIsDraggedOver(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import * as React from 'react'
import DatalinkIcon from 'enso-assets/datalink.svg'

import * as backendHooks from '#/hooks/backendHooks'
import * as eventHooks from '#/hooks/eventHooks'
import * as setAssetHooks from '#/hooks/setAssetHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'

Expand All @@ -13,6 +12,8 @@ import * as inputBindingsProvider from '#/providers/InputBindingsProvider'
import AssetEventType from '#/events/AssetEventType'
import AssetListEventType from '#/events/AssetListEventType'

import * as eventListProvider from '#/layouts/AssetsTable/EventListProvider'

import type * as column from '#/components/dashboard/column'
import EditableSpan from '#/components/EditableSpan'

Expand All @@ -36,9 +37,10 @@ export interface DatalinkNameColumnProps extends column.AssetColumnProps {}
* This should never happen. */
export default function DatalinkNameColumn(props: DatalinkNameColumnProps) {
const { item, setItem, selected, state, rowState, setRowState, isEditable } = props
const { backend, assetEvents, dispatchAssetListEvent, setIsAssetPanelTemporarilyVisible } = state
const { backend, setIsAssetPanelTemporarilyVisible } = state
const toastAndLog = toastAndLogHooks.useToastAndLog()
const inputBindings = inputBindingsProvider.useInputBindings()
const dispatchAssetListEvent = eventListProvider.useDispatchAssetListEvent()
if (item.type !== backendModule.AssetType.datalink) {
// eslint-disable-next-line no-restricted-syntax
throw new Error('`DatalinkNameColumn` can only display Datalinks.')
Expand All @@ -61,9 +63,8 @@ export default function DatalinkNameColumn(props: DatalinkNameColumnProps) {
await Promise.resolve(null)
}

eventHooks.useEventHandler(
assetEvents,
async event => {
eventListProvider.useAssetEventListener(async event => {
if (isEditable) {
switch (event.type) {
case AssetEventType.newProject:
case AssetEventType.newFolder:
Expand Down Expand Up @@ -118,9 +119,8 @@ export default function DatalinkNameColumn(props: DatalinkNameColumnProps) {
break
}
}
},
{ isDisabled: !isEditable }
)
}
}, item.initialAssetEvents)

const handleClick = inputBindings.handler({
editName: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import FolderArrowIcon from 'enso-assets/folder_arrow.svg'
import FolderIcon from 'enso-assets/folder.svg'

import * as backendHooks from '#/hooks/backendHooks'
import * as eventHooks from '#/hooks/eventHooks'
import * as setAssetHooks from '#/hooks/setAssetHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'

Expand All @@ -15,6 +14,8 @@ import * as textProvider from '#/providers/TextProvider'
import AssetEventType from '#/events/AssetEventType'
import AssetListEventType from '#/events/AssetListEventType'

import * as eventListProvider from '#/layouts/AssetsTable/EventListProvider'

import * as ariaComponents from '#/components/AriaComponents'
import type * as column from '#/components/dashboard/column'
import EditableSpan from '#/components/EditableSpan'
Expand Down Expand Up @@ -42,11 +43,12 @@ export interface DirectoryNameColumnProps extends column.AssetColumnProps {}
* This should never happen. */
export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
const { item, setItem, selected, state, rowState, setRowState, isEditable } = props
const { backend, selectedKeys, assetEvents, dispatchAssetListEvent, nodeMap } = state
const { backend, selectedKeys, nodeMap } = state
const { doToggleDirectoryExpansion } = state
const toastAndLog = toastAndLogHooks.useToastAndLog()
const { getText } = textProvider.useText()
const inputBindings = inputBindingsProvider.useInputBindings()
const dispatchAssetListEvent = eventListProvider.useDispatchAssetListEvent()
if (item.type !== backendModule.AssetType.directory) {
// eslint-disable-next-line no-restricted-syntax
throw new Error('`DirectoryNameColumn` can only display folders.')
Expand Down Expand Up @@ -87,9 +89,8 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
}
}

eventHooks.useEventHandler(
assetEvents,
async event => {
eventListProvider.useAssetEventListener(async event => {
if (isEditable) {
switch (event.type) {
case AssetEventType.newProject:
case AssetEventType.uploadFiles:
Expand Down Expand Up @@ -138,9 +139,8 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) {
break
}
}
},
{ isDisabled: !isEditable }
)
}
}, item.initialAssetEvents)

const handleClick = inputBindings.handler({
editName: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import * as React from 'react'

import * as backendHooks from '#/hooks/backendHooks'
import * as eventHooks from '#/hooks/eventHooks'
import * as setAssetHooks from '#/hooks/setAssetHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'

Expand All @@ -11,6 +10,8 @@ import * as inputBindingsProvider from '#/providers/InputBindingsProvider'
import AssetEventType from '#/events/AssetEventType'
import AssetListEventType from '#/events/AssetListEventType'

import * as eventListProvider from '#/layouts/AssetsTable/EventListProvider'

import type * as column from '#/components/dashboard/column'
import EditableSpan from '#/components/EditableSpan'
import SvgMask from '#/components/SvgMask'
Expand All @@ -37,9 +38,10 @@ export interface FileNameColumnProps extends column.AssetColumnProps {}
* This should never happen. */
export default function FileNameColumn(props: FileNameColumnProps) {
const { item, setItem, selected, state, rowState, setRowState, isEditable } = props
const { backend, nodeMap, assetEvents, dispatchAssetListEvent } = state
const { backend, nodeMap } = state
const toastAndLog = toastAndLogHooks.useToastAndLog()
const inputBindings = inputBindingsProvider.useInputBindings()
const dispatchAssetListEvent = eventListProvider.useDispatchAssetListEvent()
if (item.type !== backendModule.AssetType.file) {
// eslint-disable-next-line no-restricted-syntax
throw new Error('`FileNameColumn` can only display files.')
Expand Down Expand Up @@ -78,9 +80,8 @@ export default function FileNameColumn(props: FileNameColumnProps) {
}
}

eventHooks.useEventHandler(
assetEvents,
async event => {
eventListProvider.useAssetEventListener(async event => {
if (isEditable) {
switch (event.type) {
case AssetEventType.newProject:
case AssetEventType.newFolder:
Expand Down Expand Up @@ -138,9 +139,8 @@ export default function FileNameColumn(props: FileNameColumnProps) {
break
}
}
},
{ isDisabled: !isEditable }
)
}
}, item.initialAssetEvents)

const handleClick = inputBindings.handler({
editName: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import * as React from 'react'
import NetworkIcon from 'enso-assets/network.svg'

import * as backendHooks from '#/hooks/backendHooks'
import * as eventHooks from '#/hooks/eventHooks'
import * as setAssetHooks from '#/hooks/setAssetHooks'
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'

Expand All @@ -15,6 +14,8 @@ import * as textProvider from '#/providers/TextProvider'
import AssetEventType from '#/events/AssetEventType'
import AssetListEventType from '#/events/AssetListEventType'

import * as eventListProvider from '#/layouts/AssetsTable/EventListProvider'

import type * as column from '#/components/dashboard/column'
import ProjectIcon from '#/components/dashboard/ProjectIcon'
import EditableSpan from '#/components/EditableSpan'
Expand Down Expand Up @@ -57,12 +58,13 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
backendType,
isOpened,
} = props
const { backend, selectedKeys, assetEvents, dispatchAssetListEvent } = state
const { backend, selectedKeys } = state
const { nodeMap, doOpenEditor } = state
const toastAndLog = toastAndLogHooks.useToastAndLog()
const { user } = authProvider.useNonPartialUserSession()
const { getText } = textProvider.useText()
const inputBindings = inputBindingsProvider.useInputBindings()
const dispatchAssetListEvent = eventListProvider.useDispatchAssetListEvent()

if (item.type !== backendModule.AssetType.project) {
// eslint-disable-next-line no-restricted-syntax
Expand Down Expand Up @@ -123,9 +125,8 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
}
}

eventHooks.useEventHandler(
assetEvents,
async event => {
eventListProvider.useAssetEventListener(async event => {
if (isEditable) {
switch (event.type) {
case AssetEventType.newFolder:
case AssetEventType.newDatalink:
Expand Down Expand Up @@ -243,7 +244,11 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
setAsset(object.merge(asset, { title: listedProject.packageName, id: projectId }))
} else {
const createdFile = await uploadFileMutation.mutateAsync([
{ fileId, fileName: `${title}.${extension}`, parentDirectoryId: asset.parentId },
{
fileId,
fileName: `${title}.${extension}`,
parentDirectoryId: asset.parentId,
},
file,
])
const project = createdFile.project
Expand Down Expand Up @@ -278,9 +283,8 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) {
break
}
}
},
{ isDisabled: !isEditable }
)
}
}, item.initialAssetEvents)

const handleClick = inputBindings.handler({
editName: () => {
Expand Down
Loading

0 comments on commit 1514381

Please sign in to comment.