diff --git a/web/app/components/workflow/skill/hooks/use-sqlite-database.ts b/web/app/components/workflow/skill/hooks/use-sqlite-database.ts index b9528d8ce7..47bbc4f92c 100644 --- a/web/app/components/workflow/skill/hooks/use-sqlite-database.ts +++ b/web/app/components/workflow/skill/hooks/use-sqlite-database.ts @@ -38,7 +38,6 @@ type SQLiteAction | { type: 'error', error: Error } const TABLES_QUERY = 'SELECT name FROM sqlite_master WHERE type=\'table\' AND name NOT LIKE \'sqlite_%\' ORDER BY name' -const DEFAULT_ROW_LIMIT = 200 let sqliteClientPromise: Promise | null = null @@ -218,10 +217,10 @@ export function useSQLiteDatabase(downloadUrl: string | undefined): UseSQLiteDat if (!state.tables.includes(tableName)) return null - const rowLimit = Number.isFinite(limit) && limit && limit > 0 + const resolvedLimit = Number.isFinite(limit) && limit && limit > 0 ? Math.floor(limit) - : DEFAULT_ROW_LIMIT - const cacheKey = `${tableName}:${rowLimit}` + : null + const cacheKey = `${tableName}:${resolvedLimit ?? 'all'}` const cached = cacheRef.current.get(cacheKey) if (cached) return cached @@ -229,7 +228,7 @@ export function useSQLiteDatabase(downloadUrl: string | undefined): UseSQLiteDat const safeName = tableName.replaceAll('"', '""') const result = await client.sqlite3.execWithParams( db, - `SELECT * FROM "${safeName}" LIMIT ${rowLimit}`, + `SELECT * FROM "${safeName}"${resolvedLimit ? ` LIMIT ${resolvedLimit}` : ''}`, [], ) const data: SQLiteQueryResult = { diff --git a/web/app/components/workflow/skill/viewer/sqlite-file-preview/data-table.tsx b/web/app/components/workflow/skill/viewer/sqlite-file-preview/data-table.tsx index 909665b503..09b273d88b 100644 --- a/web/app/components/workflow/skill/viewer/sqlite-file-preview/data-table.tsx +++ b/web/app/components/workflow/skill/viewer/sqlite-file-preview/data-table.tsx @@ -1,6 +1,7 @@ import type { TFunction } from 'i18next' -import type { FC } from 'react' +import type { FC, RefObject } from 'react' import type { SQLiteValue } from '../../hooks/use-sqlite-database' +import { useVirtualizer } from '@tanstack/react-virtual' import * as React from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -9,6 +10,7 @@ import { cn } from '@/utils/classnames' type DataTableProps = { columns: string[] values: SQLiteValue[][] + scrollRef: RefObject } const MAX_CELL_LENGTH = 120 @@ -29,38 +31,25 @@ const truncateValue = (value: string): string => { return `${value.slice(0, MAX_CELL_LENGTH)}...` } -const DataTable: FC = ({ columns, values }) => { +const DataTable: FC = ({ columns, values, scrollRef }) => { const { t } = useTranslation('workflow') const keyColumnIndex = useMemo(() => { const candidates = new Set(['id', 'rowid', 'uuid']) return columns.findIndex(column => candidates.has(column.toLowerCase())) }, [columns]) - const rows = useMemo(() => { - return values.map((row) => { - const rowKey = keyColumnIndex >= 0 - ? String(row[keyColumnIndex] ?? '') - : row.map((value) => { - if (value instanceof Uint8Array) - return `blob:${value.byteLength}` - return String(value ?? '') - }).join('|') + const rowVirtualizer = useVirtualizer({ + count: values.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => 32, + overscan: 8, + }) - const cells = row.map((value) => { - const rawValue = formatValue(value, t) - return { - rawValue, - displayValue: truncateValue(rawValue), - isNull: value === null, - } - }) - - return { - key: rowKey, - cells, - } - }) - }, [keyColumnIndex, t, values]) + const virtualRows = rowVirtualizer.getVirtualItems() + const paddingTop = virtualRows.length > 0 ? virtualRows[0].start : 0 + const paddingBottom = virtualRows.length > 0 + ? rowVirtualizer.getTotalSize() - virtualRows[virtualRows.length - 1].end + : 0 return ( @@ -77,24 +66,45 @@ const DataTable: FC = ({ columns, values }) => { - {rows.map(row => ( - - {row.cells.map((cell, cellIndex) => ( - - ))} + {paddingTop > 0 && ( + + - ))} + )} + {virtualRows.map((virtualRow) => { + const row = values[virtualRow.index] + const rowKey = keyColumnIndex >= 0 + ? String(row[keyColumnIndex] ?? virtualRow.index) + : String(virtualRow.index) + + return ( + + {row.map((value, cellIndex) => { + const rawValue = formatValue(value, t) + const displayValue = truncateValue(rawValue) + return ( + + ) + })} + + ) + })} + {paddingBottom > 0 && ( + + + )}
-
- {cell.displayValue} -
-
+
+ {displayValue} +
+
) diff --git a/web/app/components/workflow/skill/viewer/sqlite-file-preview/index.tsx b/web/app/components/workflow/skill/viewer/sqlite-file-preview/index.tsx index 1b34b298a1..f32db83c8e 100644 --- a/web/app/components/workflow/skill/viewer/sqlite-file-preview/index.tsx +++ b/web/app/components/workflow/skill/viewer/sqlite-file-preview/index.tsx @@ -1,14 +1,12 @@ import type { FC } from 'react' import * as React from 'react' -import { useEffect, useMemo, useReducer, useState } from 'react' +import { useEffect, useMemo, useReducer, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' import { useSQLiteDatabase } from '../../hooks/use-sqlite-database' import DataTable from './data-table' import TableSelector from './table-selector' -const ROW_LIMIT = 200 - type SQLiteFilePreviewProps = { downloadUrl: string } @@ -19,6 +17,7 @@ const SQLiteFilePreview: FC = ({ const { t } = useTranslation('workflow') const { tables, isLoading, error, queryTable } = useSQLiteDatabase(downloadUrl) const [selectedTableId, setSelectedTableId] = useState('') + const tableScrollRef = useRef(null) const [tableState, dispatch] = useReducer(( current: { data: Awaited> | null @@ -85,7 +84,7 @@ const SQLiteFilePreview: FC = ({ dispatch({ type: 'loading' }) try { - const data = await queryTable(selectedTable, ROW_LIMIT) + const data = await queryTable(selectedTable) if (!cancelled) dispatch({ type: 'success', data }) } @@ -141,7 +140,7 @@ const SQLiteFilePreview: FC = ({ } return ( -
+
= ({ isLoading={tableState.isLoading} />
-
+
{tableState.isLoading ? (
@@ -165,20 +167,21 @@ const SQLiteFilePreview: FC = ({
) - : (tableState.data && tableState.data.values.length > 0) - ? ( - - ) - : ( -
- - {t('skillSidebar.sqlitePreview.emptyRows')} - -
- )} + : tableState.data + ? ( + + ) + : ( +
+ + {t('skillSidebar.sqlitePreview.emptyRows')} + +
+ )}
) diff --git a/web/package.json b/web/package.json index ec4afb122a..cde8f0231e 100644 --- a/web/package.json +++ b/web/package.json @@ -81,6 +81,7 @@ "@tailwindcss/typography": "0.5.19", "@tanstack/react-form": "1.23.7", "@tanstack/react-query": "5.90.5", + "@tanstack/react-virtual": "3.13.18", "abcjs": "6.5.2", "ahooks": "3.9.5", "class-variance-authority": "0.7.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 9087715f4d..bdd8c86a85 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -135,6 +135,9 @@ importers: '@tanstack/react-query': specifier: 5.90.5 version: 5.90.5(react@19.2.3) + '@tanstack/react-virtual': + specifier: 3.13.18 + version: 3.13.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3) abcjs: specifier: 6.5.2 version: 6.5.2 @@ -3476,8 +3479,8 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/react-virtual@3.13.13': - resolution: {integrity: sha512-4o6oPMDvQv+9gMi8rE6gWmsOjtUZUYIJHv7EB+GblyYdi8U6OqLl8rhHWIUZSL1dUU2dPwTdTgybCKf9EjIrQg==} + '@tanstack/react-virtual@3.13.18': + resolution: {integrity: sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -3485,8 +3488,8 @@ packages: '@tanstack/store@0.7.7': resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} - '@tanstack/virtual-core@3.13.13': - resolution: {integrity: sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA==} + '@tanstack/virtual-core@3.13.18': + resolution: {integrity: sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==} '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} @@ -10154,7 +10157,7 @@ snapshots: '@floating-ui/react': 0.26.28(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/focus': 3.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/interactions': 3.25.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/react-virtual': 3.13.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-virtual': 3.13.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -11923,15 +11926,15 @@ snapshots: react-dom: 19.2.3(react@19.2.3) use-sync-external-store: 1.6.0(react@19.2.3) - '@tanstack/react-virtual@3.13.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-virtual@3.13.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/virtual-core': 3.13.13 + '@tanstack/virtual-core': 3.13.18 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@tanstack/store@0.7.7': {} - '@tanstack/virtual-core@3.13.13': {} + '@tanstack/virtual-core@3.13.18': {} '@testing-library/dom@10.4.1': dependencies: