diff --git a/web/packages/common/src/components/LeftOverflow/index.tsx b/web/packages/common/src/components/LeftOverflow/index.tsx new file mode 100644 index 0000000000..c4105a94fb --- /dev/null +++ b/web/packages/common/src/components/LeftOverflow/index.tsx @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import classnames from 'classnames'; +import { type FC } from 'react'; + +interface LeftOverflowProps { + children: string; + className?: string; +} + +/** + * Single-line text that overflows to the *left*, putting the ellipsis at the + * start so the tail of the string stays visible. Use it for values whose + * distinguishing part is at the end — file paths, versioned model names, IDs. + * + * `dir="rtl"` moves the ellipsis to the leading edge while `text-left` keeps + * short values flush left. Bidi only reorders neutral characters at the very + * edges of the string, so pass values that begin and end in a letter or digit — + * a trailing `/` or `=` would render on the opposite side. + */ +export const LeftOverflow: FC = ({ children, className }) => ( + + {children} + +); diff --git a/web/packages/common/src/components/LogViewer/index.tsx b/web/packages/common/src/components/LogViewer/index.tsx index 9d239e69ea..f0f87a380d 100644 --- a/web/packages/common/src/components/LogViewer/index.tsx +++ b/web/packages/common/src/components/LogViewer/index.tsx @@ -72,7 +72,7 @@ export const LogViewer: FC = ({ } return ( - + {!showAllLogs && hasMoreLogs && ( @@ -88,6 +88,7 @@ export const LogViewer: FC = ({ collapsible={false} rows={rows} onCopySuccess={() => success('Copied to clipboard!', { durationMs: 3000 })} + className="min-h-auto h-full" attributes={{ CodeSnippetCode: { ref: codeScrollRef, diff --git a/web/packages/common/src/components/UploadModal/SimpleFilesTable.test.tsx b/web/packages/common/src/components/UploadModal/SimpleFilesTable.test.tsx index 28454dca73..47825486fe 100644 --- a/web/packages/common/src/components/UploadModal/SimpleFilesTable.test.tsx +++ b/web/packages/common/src/components/UploadModal/SimpleFilesTable.test.tsx @@ -44,22 +44,6 @@ const createWrapper = ( describe('SimpleFilesTable', () => { beforeEach(() => { vi.clearAllMocks(); - // @tanstack/react-virtual measures elements via getBoundingClientRect to determine - // the visible row range. JSDOM returns 0 for all dimensions, causing the virtualizer - // to compute an empty visible range and render no rows. Return a fixed 56px height - // (matching VirtualizedTableContent's default rowHeight) so the virtualizer renders - // all rows within the overscan window. - vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({ - height: 56, - width: 560, - top: 0, - left: 0, - bottom: 56, - right: 560, - x: 0, - y: 0, - toJSON: () => ({}), - }); }); const mockNewFiles: UploadFile[] = [ diff --git a/web/packages/common/src/components/UploadModal/SimpleFilesTable.tsx b/web/packages/common/src/components/UploadModal/SimpleFilesTable.tsx index 03788ff2ec..9999947704 100644 --- a/web/packages/common/src/components/UploadModal/SimpleFilesTable.tsx +++ b/web/packages/common/src/components/UploadModal/SimpleFilesTable.tsx @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import * as DataView from '@nemo/common/src/components/DataView/internal'; +import { StudioDataView } from '@nemo/common/src/components/DataView/StudioDataView'; +import { LeftOverflow } from '@nemo/common/src/components/LeftOverflow'; import { useUploadModalContext } from '@nemo/common/src/components/UploadModal/Context/useUploadModalContext'; import { useInlinePickerSlot } from '@nemo/common/src/components/UploadModal/InlinePickerSlot'; import { UploadFile } from '@nemo/common/src/components/UploadModal/types'; @@ -17,7 +19,7 @@ import { Text, } from '@nvidia/foundations-react-core'; import { CircleAlert } from 'lucide-react'; -import { type ComponentProps, useCallback, useMemo, useRef } from 'react'; +import { type ComponentProps, useCallback, useEffect, useMemo, useRef } from 'react'; type FileRow = { id: string; @@ -83,17 +85,40 @@ export const SimpleFilesTable = () => { [visibleFiles, invalidFileMode, allowedExtensions] ); - const dataViewState = DataView.useDataViewState(); + const dataViewState = DataView.useDataViewState({ pagination: { pageSize: 10 } }); + + const searchTerm = dataViewState.searchBar.state.trim().toLowerCase(); + const matchingRows = useMemo( + () => + searchTerm ? fileRows.filter((row) => row.name.toLowerCase().includes(searchTerm)) : fileRows, + [fileRows, searchTerm] + ); + const { pageIndex, pageSize } = dataViewState.pagination.state; + const safePageIndex = Math.min( + pageIndex, + Math.max(0, Math.ceil(matchingRows.length / pageSize) - 1) + ); + const pageRows = useMemo( + () => matchingRows.slice(safePageIndex * pageSize, safePageIndex * pageSize + pageSize), + [matchingRows, safePageIndex, pageSize] + ); + + const setPagination = dataViewState.pagination.set; + useEffect(() => { + if (pageIndex !== safePageIndex) { + setPagination((prev) => ({ ...prev, pageIndex: safePageIndex })); + } + }, [pageIndex, safePageIndex, setPagination]); const makeColumns = useCallback>['makeColumns']>( (col) => [ col.display({ - id: 'select', + id: 'row-selection', header: () => null, - size: 40, - maxSize: 40, - minSize: 40, - meta: { alignment: 'center' as const }, + size: 48, + maxSize: 48, + minSize: 48, + meta: { alignment: 'center' as const, _isPrebuiltColumn: true, _isSizeInitialized: true }, cell: ({ row }) => allowMultipleFileSelection ? ( { ), }), - col.accessor('name', { header: 'Name' }), + col.accessor('name', { + header: 'Name', + cell: (ctx) => {ctx.getValue()}, + }), col.accessor('size', { header: 'Size', size: 120, + maxSize: 120, + minSize: 120, + meta: { _isSizeInitialized: true }, cell: (ctx) => formatFileSize(ctx.getValue()), }), ], @@ -132,26 +163,36 @@ export const SimpleFilesTable = () => { return ( - {/* Name column fills the row; Size (col 3) is pinned to 120px. */} -
- { - const file = fileRows.find((r) => r.id === id); - if (file) dispatch({ type: 'TOGGLE_FILE_SELECTION', payload: file.uploadFile }); + { + const file = fileRows.find((r) => r.id === id); + if (file) dispatch({ type: 'TOGGLE_FILE_SELECTION', payload: file.uploadFile }); + }} + > + + dataViewState={dataViewState} + makeColumns={makeColumns} + searchField="name" + maxTwoLines={false} + onRowClick={(row) => { + if (!row.isDisabled) { + dispatch({ type: 'TOGGLE_FILE_SELECTION', payload: row.uploadFile }); + } }} - > - row.id }} - > - - - -
+ attributes={{ + DataViewRoot: { + data: pageRows, + totalCount: matchingRows.length, + reactTableOptions: { getRowId: (row) => row.id }, + }, + DataViewSearchBar: { placeholder: 'Search files…' }, + DataViewPagination: { pageSizeOptions: [10, 25, 50] }, + }} + /> + {disabledFilesMessage ? (