mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 08:48:10 +08:00
perf(web): virtualize the Web Apps sidebar list again (#41397)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
parent
3ce262de05
commit
2a554ab2d1
@ -174,6 +174,8 @@ const mockProviderContextState = vi.hoisted(() => ({
|
||||
} as Partial<ProviderContextState>,
|
||||
}))
|
||||
|
||||
vi.mock('@tanstack/react-virtual')
|
||||
|
||||
vi.mock('@/features/agent-v2/feature-flag', () => ({
|
||||
isAgentV2Enabled: () => mockIsAgentV2Enabled(),
|
||||
}))
|
||||
|
||||
@ -27,8 +27,9 @@ import {
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { keepPreviousData, useInfiniteQuery, useMutation } from '@tanstack/react-query'
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { Fragment, useRef, useState } from 'react'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { InfiniteScrollSentinel } from '@/app/components/base/infinite-scroll-sentinel'
|
||||
@ -43,12 +44,27 @@ import { hasPermission } from '@/utils/permission'
|
||||
|
||||
const emptyInstalledApps: InstalledAppResponse[] = []
|
||||
|
||||
const appNavItemHeight = 32
|
||||
const appNavItemGap = 2
|
||||
const appNavSeparatorHeight = 16.5
|
||||
|
||||
const getPreloadDistance = (scrollContainer: Element) =>
|
||||
Math.max(160, Math.min(scrollContainer.clientHeight * 0.25, 320))
|
||||
|
||||
const selectInstalledApps = (data: InfiniteData<InstalledAppListResponse, string | undefined>) =>
|
||||
data.pages.flatMap((page) => page.installed_apps)
|
||||
|
||||
type WebAppListRow =
|
||||
| {
|
||||
key: string
|
||||
kind: 'app'
|
||||
app: InstalledAppResponse
|
||||
}
|
||||
| {
|
||||
key: string
|
||||
kind: 'separator'
|
||||
}
|
||||
|
||||
const WebAppsSectionContent = () => {
|
||||
const { t } = useTranslation()
|
||||
const pathname = usePathname()
|
||||
@ -83,7 +99,34 @@ const WebAppsSectionContent = () => {
|
||||
consoleQuery.installedApps.byInstalledAppId.patch.mutationOptions(),
|
||||
)
|
||||
|
||||
const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length
|
||||
const webAppRows = useMemo<WebAppListRow[]>(() => {
|
||||
const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length
|
||||
|
||||
return installedApps.flatMap((app, index) => {
|
||||
const rows: WebAppListRow[] = [{ key: app.id, kind: 'app', app }]
|
||||
|
||||
if (index === pinnedAppsCount - 1 && index !== installedApps.length - 1)
|
||||
rows.push({ key: `${app.id}-separator`, kind: 'separator' })
|
||||
|
||||
return rows
|
||||
})
|
||||
}, [installedApps])
|
||||
const getWebAppRowKey = useCallback(
|
||||
(index: number) => webAppRows[index]?.key ?? index,
|
||||
[webAppRows],
|
||||
)
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: webAppRows.length,
|
||||
estimateSize: (index) =>
|
||||
webAppRows[index]?.kind === 'separator' ? appNavSeparatorHeight : appNavItemHeight,
|
||||
gap: appNavItemGap,
|
||||
getItemKey: getWebAppRowKey,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
overscan: 6,
|
||||
paddingEnd: 8,
|
||||
})
|
||||
|
||||
const canLoadMore = !installedAppsQuery.isFetching && !installedAppsQuery.error
|
||||
|
||||
const handleSearchTextChange = (value: string) => {
|
||||
@ -132,7 +175,6 @@ const WebAppsSectionContent = () => {
|
||||
|
||||
const renderAppNavItem = (installedApp: (typeof installedApps)[number]) => (
|
||||
<AppNavItem
|
||||
key={installedApp.id}
|
||||
app={installedApp}
|
||||
ariaLabel={t(($) => $['mainNav.webApps.openApp'], {
|
||||
ns: 'common',
|
||||
@ -143,6 +185,11 @@ const WebAppsSectionContent = () => {
|
||||
onDelete={setUninstallDialogAppId}
|
||||
/>
|
||||
)
|
||||
const renderRow = (row: WebAppListRow) => {
|
||||
if (row.kind === 'separator') return <Divider />
|
||||
|
||||
return renderAppNavItem(row.app)
|
||||
}
|
||||
return (
|
||||
<Collapsible
|
||||
open={appsExpanded && searchVisible}
|
||||
@ -225,16 +272,27 @@ const WebAppsSectionContent = () => {
|
||||
{t(($) => $['mainNav.webApps.noResults'], { ns: 'common' })}
|
||||
</div>
|
||||
)}
|
||||
{installedApps.length > 0 && (
|
||||
<div className="space-y-0.5 pb-2">
|
||||
{installedApps.map((installedApp, index) => (
|
||||
<Fragment key={installedApp.id}>
|
||||
{renderAppNavItem(installedApp)}
|
||||
{index === pinnedAppsCount - 1 && index !== installedApps.length - 1 && (
|
||||
<Divider />
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
{webAppRows.length > 0 && (
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{ height: `${rowVirtualizer.getTotalSize()}px` }}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const row = webAppRows[virtualRow.index]!
|
||||
|
||||
return (
|
||||
<div
|
||||
key={virtualRow.key}
|
||||
className="absolute top-0 left-0 w-full"
|
||||
style={{
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
{renderRow(row)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{installedAppsQuery.hasNextPage && (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user