From 709c940bdb19d43fa493f4a1aa607f9229ad3bc8 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Tue, 28 Apr 2026 02:02:56 +0530 Subject: [PATCH 1/2] fix(lightspeed): improve notebook upload modal and MessageBar UX Signed-off-by: its-mitesh-kumar --- .../.changeset/notebook-upload-modal-ux.md | 5 + .../plugins/lightspeed/report-alpha.api.md | 4 + .../__tests__/AddDocumentModal.test.tsx | 281 ++++++++++++++++++ .../__tests__/FileListItem.test.tsx | 133 +++++++++ .../components/notebooks/AddDocumentModal.tsx | 176 ++++++++--- .../src/components/notebooks/FileListItem.tsx | 125 ++++++++ .../src/components/notebooks/NotebookView.tsx | 35 ++- .../notebooks/OverwriteConfirmModal.tsx | 1 + .../lightspeed/src/translations/ref.ts | 5 + 9 files changed, 719 insertions(+), 46 deletions(-) create mode 100644 workspaces/lightspeed/.changeset/notebook-upload-modal-ux.md create mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AddDocumentModal.test.tsx create mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/FileListItem.test.tsx create mode 100644 workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileListItem.tsx diff --git a/workspaces/lightspeed/.changeset/notebook-upload-modal-ux.md b/workspaces/lightspeed/.changeset/notebook-upload-modal-ux.md new file mode 100644 index 00000000000..f663adee8c7 --- /dev/null +++ b/workspaces/lightspeed/.changeset/notebook-upload-modal-ux.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-lightspeed': minor +--- + +Improved notebook upload modal and MessageBar UX. diff --git a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md index f954c7c2b76..4bb031f10d4 100644 --- a/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md +++ b/workspaces/lightspeed/plugins/lightspeed/report-alpha.api.md @@ -230,6 +230,7 @@ export const lightspeedTranslationRef: TranslationRef< readonly 'notebook.view.upload.heading': string; readonly 'notebook.view.upload.action': string; readonly 'notebook.view.input.placeholder': string; + readonly 'notebook.view.input.disabledTooltip': string; readonly 'notebook.view.sidebar.collapse': string; readonly 'notebook.view.sidebar.expand': string; readonly 'notebook.view.sidebar.resize': string; @@ -241,6 +242,9 @@ export const lightspeedTranslationRef: TranslationRef< readonly 'notebook.upload.modal.browseButton': string; readonly 'notebook.upload.modal.separator': string; readonly 'notebook.upload.modal.infoText': string; + readonly 'notebook.upload.modal.selectedFiles': string; + readonly 'notebook.upload.modal.addButton': string; + readonly 'notebook.upload.modal.removeFile': string; readonly 'notebook.upload.error.unsupportedType': string; readonly 'notebook.upload.error.fileTooLarge': string; readonly 'notebook.upload.error.tooManyFiles': string; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AddDocumentModal.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AddDocumentModal.test.tsx new file mode 100644 index 00000000000..a2e0e29bf1f --- /dev/null +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/AddDocumentModal.test.tsx @@ -0,0 +1,281 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; + +import { mockUseTranslation } from '../../test-utils/mockTranslations'; +import { AddDocumentModal } from '../notebooks/AddDocumentModal'; + +jest.mock('../../hooks/useTranslation', () => ({ + useTranslation: jest.fn(() => mockUseTranslation()), +})); + +const mockMutateAsync = jest.fn(); +jest.mock('../../hooks/notebooks/useUploadDocument', () => ({ + useUploadDocument: () => ({ + mutateAsync: mockMutateAsync, + }), +})); + +describe('AddDocumentModal', () => { + const defaultProps = { + isOpen: true, + onClose: jest.fn(), + sessionId: 'test-session-id', + existingDocumentNames: [], + onFilesUploading: jest.fn(), + onUploadStarted: jest.fn(), + onUploadFailed: jest.fn(), + onDuplicatesFound: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockMutateAsync.mockResolvedValue({ document_id: 'test-doc-id' }); + }); + + it('should render the modal when open', () => { + render(); + + expect(screen.getByText('Add a document to Notebook')).toBeInTheDocument(); + expect(screen.getByText('Drag and drop files here')).toBeInTheDocument(); + }); + + it('should not render when isOpen is false', () => { + render(); + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + it('should render Cancel and Add buttons', () => { + render(); + + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Add (0)' })).toBeInTheDocument(); + }); + + it('should have Add button disabled when no files selected', () => { + render(); + + const addButton = screen.getByRole('button', { name: 'Add (0)' }); + expect(addButton).toBeDisabled(); + }); + + it('should call onClose when Cancel button is clicked', () => { + render(); + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + fireEvent.click(cancelButton); + + expect(defaultProps.onClose).toHaveBeenCalledTimes(1); + }); + + it('should call onClose when close icon is clicked', () => { + render(); + + const closeButton = screen.getByRole('button', { name: 'Close' }); + fireEvent.click(closeButton); + + expect(defaultProps.onClose).toHaveBeenCalledTimes(1); + }); + + it('should display file list when files are dropped', async () => { + render(); + + const dropzone = screen + .getByText('Drag and drop files here') + .closest('div'); + const file = new File(['content'], 'test-file.txt', { type: 'text/plain' }); + + fireEvent.drop(dropzone!, { + dataTransfer: { + files: [file], + types: ['Files'], + }, + }); + + await waitFor(() => { + expect(screen.getByText('test-file.txt')).toBeInTheDocument(); + }); + }); + + it('should update Add button count when files are selected', async () => { + render(); + + const dropzone = screen + .getByText('Drag and drop files here') + .closest('div'); + const file = new File(['content'], 'test-file.txt', { type: 'text/plain' }); + + fireEvent.drop(dropzone!, { + dataTransfer: { + files: [file], + types: ['Files'], + }, + }); + + await waitFor(() => { + expect( + screen.getByRole('button', { name: 'Add (1)' }), + ).toBeInTheDocument(); + }); + }); + + it('should not auto-close modal after file drop', async () => { + render(); + + const dropzone = screen + .getByText('Drag and drop files here') + .closest('div'); + const file = new File(['content'], 'test-file.txt', { type: 'text/plain' }); + + fireEvent.drop(dropzone!, { + dataTransfer: { + files: [file], + types: ['Files'], + }, + }); + + await waitFor(() => { + expect(screen.getByText('test-file.txt')).toBeInTheDocument(); + }); + + expect(defaultProps.onClose).not.toHaveBeenCalled(); + }); + + it('should trigger upload and close when Add button is clicked', async () => { + render(); + + const dropzone = screen + .getByText('Drag and drop files here') + .closest('div'); + const file = new File(['content'], 'test-file.txt', { type: 'text/plain' }); + + fireEvent.drop(dropzone!, { + dataTransfer: { + files: [file], + types: ['Files'], + }, + }); + + await waitFor(() => { + expect( + screen.getByRole('button', { name: 'Add (1)' }), + ).not.toBeDisabled(); + }); + + fireEvent.click(screen.getByRole('button', { name: 'Add (1)' })); + + await waitFor(() => { + expect(defaultProps.onFilesUploading).toHaveBeenCalledWith([file]); + expect(mockMutateAsync).toHaveBeenCalledWith({ + sessionId: 'test-session-id', + file, + }); + expect(defaultProps.onClose).toHaveBeenCalled(); + }); + }); + + it('should allow removing files from the list', async () => { + render(); + + const dropzone = screen + .getByText('Drag and drop files here') + .closest('div'); + const file = new File(['content'], 'test-file.txt', { type: 'text/plain' }); + + fireEvent.drop(dropzone!, { + dataTransfer: { + files: [file], + types: ['Files'], + }, + }); + + await waitFor(() => { + expect(screen.getByText('test-file.txt')).toBeInTheDocument(); + }); + + const removeButton = screen.getByRole('button', { + name: 'Remove test-file.txt', + }); + fireEvent.click(removeButton); + + await waitFor(() => { + expect(screen.queryByText('test-file.txt')).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Add (0)' })).toBeDisabled(); + }); + }); + + it('should clear selected files when modal is closed', async () => { + const { rerender } = render(); + + const dropzone = screen + .getByText('Drag and drop files here') + .closest('div'); + const file = new File(['content'], 'test-file.txt', { type: 'text/plain' }); + + fireEvent.drop(dropzone!, { + dataTransfer: { + files: [file], + types: ['Files'], + }, + }); + + await waitFor(() => { + expect(screen.getByText('test-file.txt')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + + rerender(); + + expect(screen.queryByText('test-file.txt')).not.toBeInTheDocument(); + }); + + it('should call onDuplicatesFound for files that already exist', async () => { + render( + , + ); + + const dropzone = screen + .getByText('Drag and drop files here') + .closest('div'); + const existingFile = new File(['content'], 'existing-file.txt', { + type: 'text/plain', + }); + const newFile = new File(['content'], 'new-file.txt', { + type: 'text/plain', + }); + + fireEvent.drop(dropzone!, { + dataTransfer: { + files: [existingFile, newFile], + types: ['Files'], + }, + }); + + await waitFor(() => { + expect(defaultProps.onDuplicatesFound).toHaveBeenCalledWith([ + existingFile, + ]); + expect(screen.getByText('new-file.txt')).toBeInTheDocument(); + expect(screen.queryByText('existing-file.txt')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/FileListItem.test.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/FileListItem.test.tsx new file mode 100644 index 00000000000..4438fb7f756 --- /dev/null +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/__tests__/FileListItem.test.tsx @@ -0,0 +1,133 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { fireEvent, render, screen } from '@testing-library/react'; + +import { FileListItem } from '../notebooks/FileListItem'; + +describe('FileListItem', () => { + const createFile = ( + name: string, + size: number, + type: string = 'text/plain', + ) => new File(['x'.repeat(size)], name, { type }); + + const defaultProps = { + file: createFile('test-file.txt', 1024), + onRemove: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render the file name', () => { + render(); + + expect(screen.getByText('test-file.txt')).toBeInTheDocument(); + }); + + it('should render the file type icon', () => { + render(); + + expect(screen.getByText('txt')).toBeInTheDocument(); + }); + + it('should render the file size in KB', () => { + render(); + + expect(screen.getByText('1 KB')).toBeInTheDocument(); + }); + + it('should render the file size in MB for larger files', () => { + const largeFile = createFile('large-file.pdf', 5 * 1024 * 1024); + render(); + + expect(screen.getByText('5 MB')).toBeInTheDocument(); + }); + + it('should render 0 B for empty files', () => { + const emptyFile = createFile('empty.txt', 0); + render(); + + expect(screen.getByText('0 B')).toBeInTheDocument(); + }); + + it('should call onRemove when remove button is clicked', () => { + render(); + + const removeButton = screen.getByRole('button', { name: 'Remove file' }); + fireEvent.click(removeButton); + + expect(defaultProps.onRemove).toHaveBeenCalledTimes(1); + }); + + it('should use custom aria-label for remove button', () => { + render( + , + ); + + expect( + screen.getByRole('button', { name: 'Remove test-file.txt' }), + ).toBeInTheDocument(); + }); + + it('should render different file type icons based on extension', () => { + const pdfFile = createFile('document.pdf', 1024); + const { rerender } = render( + , + ); + + expect(screen.getByText('pdf')).toBeInTheDocument(); + + const yamlFile = createFile('config.yaml', 1024); + rerender(); + + expect(screen.getByText('yaml')).toBeInTheDocument(); + }); + + it('should show title tooltip for long file names', () => { + const longNameFile = createFile( + 'this-is-a-very-long-filename-that-should-be-truncated.txt', + 1024, + ); + render(); + + const fileNameElement = screen.getByTitle( + 'this-is-a-very-long-filename-that-should-be-truncated.txt', + ); + expect(fileNameElement).toBeInTheDocument(); + }); + + it('should truncate long file names while preserving extension', () => { + const longNameFile = createFile( + 'this-is-a-very-long-filename-that-should-be-truncated.txt', + 1024, + ); + render(); + + expect( + screen.getByText('this-is-a-very-long-fil....txt'), + ).toBeInTheDocument(); + }); + + it('should not truncate short file names', () => { + const shortNameFile = createFile('short-name.pdf', 1024); + render(); + + expect(screen.getByText('short-name.pdf')).toBeInTheDocument(); + }); +}); diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/AddDocumentModal.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/AddDocumentModal.tsx index cfb902cc999..e35efd1b319 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/AddDocumentModal.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/AddDocumentModal.tsx @@ -20,7 +20,10 @@ import { FileRejection } from 'react-dropzone'; import { makeStyles } from '@material-ui/core/styles'; import CloseIcon from '@mui/icons-material/Close'; import Alert from '@mui/material/Alert'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import IconButton from '@mui/material/IconButton'; @@ -38,6 +41,7 @@ import { getNotebookAcceptedFileTypes, validateFiles, } from '../../utils/notebook-upload-utils'; +import { FileListItem } from './FileListItem'; const useStyles = makeStyles(theme => ({ dialogPaper: { @@ -76,6 +80,32 @@ const useStyles = makeStyles(theme => ({ 'color-mix(in srgb, var(--pf-t--global--color--brand--default) 10%, transparent)', }, }, + fileListContainer: { + marginTop: theme.spacing(2), + maxHeight: 200, + overflowY: 'auto', + }, + fileListHeader: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: theme.spacing(1), + }, + fileCount: { + fontSize: '0.875rem', + color: theme.palette.text.secondary, + }, + dialogActions: { + padding: '16px 24px', + justifyContent: 'flex-end', + gap: theme.spacing(1), + }, + addButton: { + textTransform: 'none', + }, + cancelButton: { + textTransform: 'none', + }, })); type AddDocumentModalProps = { @@ -103,14 +133,16 @@ export const AddDocumentModal = ({ const { t } = useTranslation(); const uploadMutation = useUploadDocument(); const [validationErrors, setValidationErrors] = useState([]); + const [selectedFiles, setSelectedFiles] = useState([]); + + const totalExistingAndSelected = + existingDocumentNames.length + selectedFiles.length; + const remainingSlots = NOTEBOOK_MAX_FILES - totalExistingAndSelected; const handleFileDrop = (_event: unknown, files: File[]) => { setValidationErrors([]); - const { valid, errors } = validateFiles( - files, - existingDocumentNames.length, - ); + const { valid, errors } = validateFiles(files, totalExistingAndSelected); if (errors.length > 0) { setValidationErrors(errors); @@ -119,31 +151,47 @@ export const AddDocumentModal = ({ if (valid.length === 0) return; - const existingNamesSet = new Set(existingDocumentNames); + const existingNamesSet = new Set([ + ...existingDocumentNames, + ...selectedFiles.map(f => f.name), + ]); const newFiles = valid.filter(f => !existingNamesSet.has(f.name)); - const duplicateFiles = valid.filter(f => existingNamesSet.has(f.name)); + const duplicateFiles = valid.filter(f => + existingDocumentNames.includes(f.name), + ); + + if (duplicateFiles.length > 0) { + onDuplicatesFound?.(duplicateFiles); + } if (newFiles.length > 0) { - onFilesUploading?.(newFiles); - for (const file of newFiles) { - uploadMutation - .mutateAsync({ sessionId, file }) - .then(data => { - onUploadStarted?.({ - fileName: file.name, - documentId: data.document_id, - }); - }) - .catch(() => { - onUploadFailed?.(file.name); - }); - } + setSelectedFiles(prev => [...prev, ...newFiles]); } + }; - if (duplicateFiles.length > 0) { - onDuplicatesFound?.(duplicateFiles); + const handleRemoveFile = (index: number) => { + setSelectedFiles(prev => prev.filter((_, i) => i !== index)); + }; + + const handleAddFiles = () => { + if (selectedFiles.length === 0) return; + + onFilesUploading?.(selectedFiles); + for (const file of selectedFiles) { + uploadMutation + .mutateAsync({ sessionId, file }) + .then(data => { + onUploadStarted?.({ + fileName: file.name, + documentId: data.document_id, + }); + }) + .catch(() => { + onUploadFailed?.(file.name); + }); } + setSelectedFiles([]); setValidationErrors([]); onClose(); }; @@ -158,6 +206,7 @@ export const AddDocumentModal = ({ }; const handleClose = () => { + setSelectedFiles([]); setValidationErrors([]); onClose(); }; @@ -174,6 +223,8 @@ export const AddDocumentModal = ({ {t('notebook.upload.modal.title')} + {selectedFiles.length > 0 && + ` (${selectedFiles.length}/${NOTEBOOK_MAX_FILES - existingDocumentNames.length})`} )} - - } - titleText={t('notebook.upload.modal.dragDropTitle')} - titleTextSeparator={t('notebook.upload.modal.separator')} - infoText={t('notebook.upload.modal.infoText')} - browseButtonText={t('notebook.upload.modal.browseButton')} - /> - + {remainingSlots > 0 && ( + + } + titleText={t('notebook.upload.modal.dragDropTitle')} + titleTextSeparator={t('notebook.upload.modal.separator')} + infoText={t('notebook.upload.modal.infoText')} + browseButtonText={t('notebook.upload.modal.browseButton')} + /> + + )} + + {selectedFiles.length > 0 && ( + + + + {(t as Function)('notebook.upload.modal.selectedFiles', { + count: selectedFiles.length, + max: NOTEBOOK_MAX_FILES - existingDocumentNames.length, + })} + + + {selectedFiles.map((file, index) => ( + handleRemoveFile(index)} + removeAriaLabel={(t as Function)( + 'notebook.upload.modal.removeFile', + { + fileName: file.name, + }, + )} + /> + ))} + + )} + + + + + ); }; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileListItem.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileListItem.tsx new file mode 100644 index 00000000000..d6303b7d9fd --- /dev/null +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/FileListItem.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeStyles } from '@material-ui/core/styles'; +import CloseIcon from '@mui/icons-material/Close'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; + +import { FileTypeIcon } from './FileTypeIcon'; + +const useStyles = makeStyles(theme => ({ + container: { + display: 'flex', + alignItems: 'center', + padding: '8px 12px', + borderRadius: 8, + backgroundColor: theme.palette.type === 'dark' ? '#2a2a2a' : '#f5f5f5', + marginBottom: 8, + '&:last-child': { + marginBottom: 0, + }, + }, + fileInfo: { + display: 'flex', + alignItems: 'center', + flex: 1, + minWidth: 0, + gap: 12, + }, + fileName: { + flex: 1, + minWidth: 0, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + fontSize: '0.875rem', + }, + fileSize: { + color: theme.palette.text.secondary, + fontSize: '0.75rem', + flexShrink: 0, + marginRight: 8, + }, + removeButton: { + padding: 4, + color: theme.palette.grey[600], + '&:hover': { + color: theme.palette.error.main, + }, + }, +})); + +type FileListItemProps = { + file: File; + onRemove: () => void; + removeAriaLabel?: string; +}; + +const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; +}; + +const MAX_FILENAME_LENGTH = 30; + +const truncateFileName = (fileName: string, maxLength: number): string => { + if (fileName.length <= maxLength) return fileName; + + const lastDot = fileName.lastIndexOf('.'); + const extension = lastDot >= 0 ? fileName.slice(lastDot) : ''; + const baseName = lastDot >= 0 ? fileName.slice(0, lastDot) : fileName; + + const availableLength = maxLength - extension.length - 3; + if (availableLength <= 0) return fileName; + + return `${baseName.slice(0, availableLength)}...${extension}`; +}; + +export const FileListItem = ({ + file, + onRemove, + removeAriaLabel = 'Remove file', +}: FileListItemProps) => { + const classes = useStyles(); + const displayName = truncateFileName(file.name, MAX_FILENAME_LENGTH); + + return ( + + + + + {displayName} + + + + {formatFileSize(file.size)} + + + + + + ); +}; diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx index b498d20cd65..b66e1d67355 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx @@ -628,14 +628,33 @@ export const NotebookView = ({ )} - + {documents.length === 0 ? ( + +
+ +
+
+ ) : ( + + )}
diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/OverwriteConfirmModal.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/OverwriteConfirmModal.tsx index 8e52600a287..a4f2fe7da31 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/OverwriteConfirmModal.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/OverwriteConfirmModal.tsx @@ -63,6 +63,7 @@ const useStyles = makeStyles(theme => ({ padding: `${theme.spacing(2)}px 0`, borderBottom: '1px solid var(--pf-t--global--border--color--default, #c7c7c7)', + cursor: 'pointer', }, fileName: { flex: 1, diff --git a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts index 5f494fc6dbd..216af6177f0 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts +++ b/workspaces/lightspeed/plugins/lightspeed/src/translations/ref.ts @@ -61,6 +61,8 @@ export const lightspeedMessages = { 'notebook.view.upload.heading': 'Upload a resource to get started', 'notebook.view.upload.action': 'Upload a resource', 'notebook.view.input.placeholder': 'Ask about your documents...', + 'notebook.view.input.disabledTooltip': + 'Select at least one loaded resource to start chatting', 'notebook.view.sidebar.collapse': 'Collapse sidebar', 'notebook.view.sidebar.expand': 'Expand sidebar', 'notebook.view.sidebar.resize': 'Resize sidebar', @@ -75,6 +77,9 @@ export const lightspeedMessages = { 'notebook.upload.modal.separator': 'or', 'notebook.upload.modal.infoText': 'Accepted file types: .md, .txt, .pdf, .json, .yaml, .log', + 'notebook.upload.modal.selectedFiles': '{{count}} of {{max}} files selected', + 'notebook.upload.modal.addButton': 'Add ({{count}})', + 'notebook.upload.modal.removeFile': 'Remove {{fileName}}', 'notebook.upload.error.unsupportedType': 'Upload error: Unsupported file type(s) found. Please upload only supported file types.', 'notebook.upload.error.fileTooLarge': From 1757c5b5dffb7a248f224958d96f313e349d6416 Mon Sep 17 00:00:00 2001 From: its-mitesh-kumar Date: Tue, 28 Apr 2026 11:22:39 +0530 Subject: [PATCH 2/2] toast message auto dismiss Signed-off-by: its-mitesh-kumar --- .../lightspeed/src/components/notebooks/NotebookView.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx index b66e1d67355..eab77df495d 100644 --- a/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx +++ b/workspaces/lightspeed/plugins/lightspeed/src/components/notebooks/NotebookView.tsx @@ -551,6 +551,8 @@ export const NotebookView = ({ variant={AlertVariant[variant ?? 'success']} title={title} className={classes.toastAlert} + timeout={2000} + onTimeout={() => handleRemoveToastAlert(key as React.Key)} actionClose={