Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions web/packages/common/src/components/LeftOverflow/index.tsx
Original file line number Diff line number Diff line change
@@ -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<LeftOverflowProps> = ({ children, className }) => (
<span className={classnames('block truncate text-left', className)} dir="rtl">
{children}
</span>
);
3 changes: 2 additions & 1 deletion web/packages/common/src/components/LogViewer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const LogViewer: FC<LogViewerProps> = ({
}

return (
<Block className="relative w-full min-w-0 max-w-full overflow-hidden">
<Block className="relative w-full min-w-0 max-w-full overflow-hidden h-full">
{!showAllLogs && hasMoreLogs && (
<Block className="absolute top-6 mt-[2px] left-px right-px z-10 py-5 text-center bg-[linear-gradient(to_bottom,var(--background-color-surface-sunken),transparent)]">
<Tag color="gray" kind="solid" onClick={handleLoadMore}>
Expand All @@ -88,6 +88,7 @@ export const LogViewer: FC<LogViewerProps> = ({
collapsible={false}
rows={rows}
onCopySuccess={() => success('Copied to clipboard!', { durationMs: 3000 })}
className="min-h-auto h-full"
attributes={{
CodeSnippetCode: {
ref: codeScrollRef,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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]
);
Comment thread
steramae-nvidia marked this conversation as resolved.

const setPagination = dataViewState.pagination.set;
useEffect(() => {
if (pageIndex !== safePageIndex) {
setPagination((prev) => ({ ...prev, pageIndex: safePageIndex }));
}
}, [pageIndex, safePageIndex, setPagination]);

const makeColumns = useCallback<ComponentProps<typeof DataView.Root<FileRow>>['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 ? (
<Checkbox
Expand All @@ -110,10 +135,16 @@ export const SimpleFilesTable = () => {
</RadioGroupItem>
),
}),
col.accessor('name', { header: 'Name' }),
col.accessor('name', {
header: 'Name',
cell: (ctx) => <LeftOverflow>{ctx.getValue()}</LeftOverflow>,
}),
col.accessor('size', {
header: 'Size',
size: 120,
maxSize: 120,
minSize: 120,
meta: { _isSizeInitialized: true },
cell: (ctx) => formatFileSize(ctx.getValue()),
}),
],
Expand All @@ -132,26 +163,36 @@ export const SimpleFilesTable = () => {

return (
<Stack className="min-h-0 flex-1 w-full" gap="density-md">
{/* Name column fills the row; Size (col 3) is pinned to 120px. */}
<div className="border border-base rounded-md overflow-hidden [&_tr>*:nth-child(2)]:w-full! [&_tr>*:nth-child(2)]:max-w-none! [&_tr>*:nth-child(3)]:w-[120px]! [&_tr>*:nth-child(3)]:min-w-[120px]! [&_tr>*:nth-child(3)]:max-w-[120px]!">
<RadioGroupRoot
name="simple-files-table"
value={selectedFiles[0]?.id ?? ''}
onValueChange={(id) => {
const file = fileRows.find((r) => r.id === id);
if (file) dispatch({ type: 'TOGGLE_FILE_SELECTION', payload: file.uploadFile });
<RadioGroupRoot
className="min-h-0 max-h-[60dvh] flex"
name="simple-files-table"
value={selectedFiles[0]?.id ?? ''}
onValueChange={(id) => {
const file = fileRows.find((r) => r.id === id);
if (file) dispatch({ type: 'TOGGLE_FILE_SELECTION', payload: file.uploadFile });
}}
>
<StudioDataView<FileRow>
dataViewState={dataViewState}
makeColumns={makeColumns}
searchField="name"
maxTwoLines={false}
onRowClick={(row) => {
if (!row.isDisabled) {
dispatch({ type: 'TOGGLE_FILE_SELECTION', payload: row.uploadFile });
}
}}
>
<DataView.Root
data={fileRows}
state={dataViewState}
makeColumns={makeColumns}
reactTableOptions={{ getRowId: (row) => row.id }}
>
<DataView.VirtualizedTableContent maxHeight="45dvh" />
</DataView.Root>
</RadioGroupRoot>
</div>
attributes={{
DataViewRoot: {
data: pageRows,
totalCount: matchingRows.length,
reactTableOptions: { getRowId: (row) => row.id },
},
DataViewSearchBar: { placeholder: 'Search files…' },
DataViewPagination: { pageSizeOptions: [10, 25, 50] },
}}
/>
</RadioGroupRoot>
{disabledFilesMessage ? (
<Flex gap="density-sm" align="center">
<CircleAlert className="text-feedback-warning shrink-0" />
Expand Down
Loading