diff --git a/locales/en/public.json b/locales/en/public.json index 6029357af..bb16b92ab 100644 --- a/locales/en/public.json +++ b/locales/en/public.json @@ -23,6 +23,10 @@ } }, "AgentProbeTemplates": { + "ARIA_LABELS": { + "ROW_ACTION": "agent-probe-template-action-menu", + "SEARCH_INPUT": "agent-probe-template-search" + }, "SEARCH_PLACEHOLDER": "Find by name or XML content..." }, "AllArchivedRecordingsTable": { diff --git a/src/app/Agent/AgentProbeTemplates.tsx b/src/app/Agent/AgentProbeTemplates.tsx index 2349c1652..d29c86d66 100644 --- a/src/app/Agent/AgentProbeTemplates.tsx +++ b/src/app/Agent/AgentProbeTemplates.tsx @@ -34,7 +34,6 @@ import { ModalVariant, Stack, StackItem, - TextInput, Toolbar, ToolbarContent, ToolbarGroup, @@ -45,6 +44,8 @@ import { DropdownList, MenuToggleElement, MenuToggle, + SearchInput, + Divider, } from '@patternfly/react-core'; import { SearchIcon, EllipsisVIcon, UploadIcon } from '@patternfly/react-icons'; import { @@ -291,7 +292,7 @@ export const AgentProbeTemplates: React.FC = ({ agentD } else { return ( <> - + @@ -300,13 +301,13 @@ export const AgentProbeTemplates: React.FC = ({ agentD - @@ -339,7 +340,7 @@ export const AgentProbeTemplates: React.FC = ({ agentD ))} - {...templateRows} + {templateRows} ) : ( @@ -364,6 +365,7 @@ export interface AgentProbeTemplateUploadModalProps { } export const AgentProbeTemplateUploadModal: React.FC = ({ onClose, isOpen }) => { + const { t } = useTranslation(); const addSubscription = useSubscriptions(); const context = React.useContext(ServiceContext); const submitRef = React.useRef(null); // Use ref to refer to submit trigger div @@ -476,7 +478,7 @@ export const AgentProbeTemplateUploadModal: React.FC {allOks && numOfFiles ? ( ) : ( <> @@ -486,10 +488,10 @@ export const AgentProbeTemplateUploadModal: React.FC - Submit + {t('SUBMIT', { ns: 'common' })} )} @@ -506,6 +508,7 @@ export interface AgentTemplateActionProps { } export const AgentTemplateAction: React.FC = ({ onInsert, onDelete, template }) => { + const { t } = useTranslation(); const [isOpen, setIsOpen] = React.useState(false); const actionItems = React.useMemo(() => { @@ -516,9 +519,13 @@ export const AgentTemplateAction: React.FC = ({ onInse onClick: () => onInsert && onInsert(template), isDisabled: !onInsert, }, + { + isSeparator: true, + }, { key: 'delete-template', title: 'Delete', + isDanger: true, onClick: () => onDelete(template), }, ]; @@ -528,32 +535,43 @@ export const AgentTemplateAction: React.FC = ({ onInse const dropdownItems = React.useMemo( () => - actionItems.map((action) => ( - { - setIsOpen(false); - action.onClick(); - }} - isDisabled={action.isDisabled} - > - {action.title} - - )), + actionItems.map((action, idx) => + action.isSeparator ? ( + + ) : ( + { + setIsOpen(false); + action.onClick && action.onClick(); + }} + isAriaDisabled={action.isDisabled} + isDanger={action.isDanger} + > + {action.title} + + ), + ), [actionItems, setIsOpen], ); return ( ) => ( - handleToggle(event, !isOpen)}> + handleToggle(event, !isOpen)} + > )} + onOpenChange={setIsOpen} + onOpenChangeKeys={['Escape']} isOpen={isOpen} popperProps={{ - appendTo: document.body, position: 'right', enableFlip: true, }} diff --git a/src/app/Shared/Components/FileUploads.tsx b/src/app/Shared/Components/FileUploads.tsx index 708545fc5..d78c10bce 100644 --- a/src/app/Shared/Components/FileUploads.tsx +++ b/src/app/Shared/Components/FileUploads.tsx @@ -94,11 +94,10 @@ export const MultiFileUpload: React.FC = ({ }) as FUpload, ), ]; - onFilesChange && onFilesChange(newFileUploads); return newFileUploads; }); }, - [setFileUploads, onFilesChange], + [setFileUploads], ); const handleFileReject = React.useCallback( @@ -120,12 +119,11 @@ export const MultiFileUpload: React.FC = ({ } else { setFileUploads((old) => { const newFileUploads = old.filter((fileUpload) => fileUpload.file.name !== removedFilename); - onFilesChange && onFilesChange(newFileUploads); return newFileUploads; }); } }, - [fileUploads, setFileUploads, onFilesChange], + [fileUploads, setFileUploads], ); const getProgressUpdateCallback = React.useCallback( @@ -244,6 +242,10 @@ export const MultiFileUpload: React.FC = ({ return fileUploads.sort((a, b) => a.file.name.localeCompare(b.file.name, undefined, { numeric: true })); }, [fileUploads]); + React.useEffect(() => { + onFilesChange && onFilesChange(fileUploads); + }, [onFilesChange, fileUploads]); + return ( <> {/* diff --git a/src/test/Agent/AgentProbeTemplates.test.tsx b/src/test/Agent/AgentProbeTemplates.test.tsx index 024795dec..2c0504294 100644 --- a/src/test/Agent/AgentProbeTemplates.test.tsx +++ b/src/test/Agent/AgentProbeTemplates.test.tsx @@ -24,9 +24,9 @@ import { } from '@app/Shared/Services/api.types'; import { defaultServices } from '@app/Shared/Services/Services'; import '@testing-library/jest-dom'; -import { cleanup, screen, within } from '@testing-library/react'; +import { cleanup, screen, within, act } from '@testing-library/react'; import { of, Subject } from 'rxjs'; -import { render } from '../utils'; +import { render, testT } from '../utils'; const mockMessageType = { type: 'application', subtype: 'json' } as MessageType; @@ -124,19 +124,21 @@ describe('', () => { routerConfigs: { routes: [{ path: '/events', element: }] }, }); - const uploadButton = screen.getByRole('button', { name: 'Upload' }); - expect(uploadButton).toBeInTheDocument(); - expect(uploadButton).toBeVisible(); + await act(async () => { + const uploadButton = screen.getByRole('button', { name: 'Upload' }); + expect(uploadButton).toBeInTheDocument(); + expect(uploadButton).toBeVisible(); - await user.click(uploadButton); + await user.click(uploadButton); - const modal = await screen.findByRole('dialog'); - expect(modal).toBeInTheDocument(); - expect(modal).toBeVisible(); + const modal = await screen.findByRole('dialog'); + expect(modal).toBeInTheDocument(); + expect(modal).toBeVisible(); - const modalTitle = within(modal).getByText('Create custom Probe Template'); - expect(modalTitle).toBeInTheDocument(); - expect(modalTitle).toBeVisible(); + const modalTitle = within(modal).getByText('Create custom Probe Template'); + expect(modalTitle).toBeInTheDocument(); + expect(modalTitle).toBeVisible(); + }); }); it('should upload a Probe Template when form is filled and Submit is clicked', async () => { @@ -144,51 +146,53 @@ describe('', () => { routerConfigs: { routes: [{ path: '/events', element: }] }, }); - const uploadButton = screen.getByRole('button', { name: 'Upload' }); - expect(uploadButton).toBeInTheDocument(); - expect(uploadButton).toBeVisible(); + await act(async () => { + const uploadButton = screen.getByRole('button', { name: 'Upload' }); + expect(uploadButton).toBeInTheDocument(); + expect(uploadButton).toBeVisible(); - await user.click(uploadButton); + await user.click(uploadButton); - const modal = await screen.findByRole('dialog'); - expect(modal).toBeInTheDocument(); - expect(modal).toBeVisible(); + const modal = await screen.findByRole('dialog'); + expect(modal).toBeInTheDocument(); + expect(modal).toBeVisible(); - const modalTitle = within(modal).getByText('Create custom Probe Template'); - expect(modalTitle).toBeInTheDocument(); - expect(modalTitle).toBeVisible(); + const modalTitle = within(modal).getByText('Create custom Probe Template'); + expect(modalTitle).toBeInTheDocument(); + expect(modalTitle).toBeVisible(); - const dropZoneText = within(modal).getByText('Drag a file here'); - expect(dropZoneText).toBeInTheDocument(); - expect(dropZoneText).toBeVisible(); + const dropZoneText = within(modal).getByText('Drag a file here'); + expect(dropZoneText).toBeInTheDocument(); + expect(dropZoneText).toBeVisible(); - const uploadButtonInModal = within(modal).getByText('Upload'); - expect(uploadButtonInModal).toBeInTheDocument(); - expect(uploadButtonInModal).toBeVisible(); + const uploadButtonInModal = within(modal).getByText('Upload'); + expect(uploadButtonInModal).toBeInTheDocument(); + expect(uploadButtonInModal).toBeVisible(); - const uploadInput = modal.querySelector("input[accept='.xml'][type='file']") as HTMLInputElement; - expect(uploadInput).toBeInTheDocument(); - expect(uploadInput).not.toBeVisible(); + const uploadInput = modal.querySelector("input[accept='application/xml,.xml'][type='file']") as HTMLInputElement; + expect(uploadInput).toBeInTheDocument(); + expect(uploadInput).not.toBeVisible(); - await user.click(uploadButtonInModal); - await user.upload(uploadInput, mockFileUpload); + await user.click(uploadButtonInModal); + await user.upload(uploadInput, mockFileUpload); - const submitButton = within(modal).getByText('Submit'); - expect(submitButton).toBeInTheDocument(); - expect(submitButton).toBeVisible(); - expect(submitButton).not.toBeDisabled(); + const submitButton = within(modal).getByText('Submit'); + expect(submitButton).toBeInTheDocument(); + expect(submitButton).toBeVisible(); + expect(submitButton).not.toBeDisabled(); - await user.click(submitButton); + await user.click(submitButton); - expect(uploadRequestSpy).toHaveBeenCalledTimes(1); - expect(uploadRequestSpy).toHaveBeenCalledWith(mockFileUpload, expect.any(Function), expect.any(Subject)); + expect(uploadRequestSpy).toHaveBeenCalledTimes(1); + expect(uploadRequestSpy).toHaveBeenCalledWith(mockFileUpload, expect.any(Function), expect.any(Subject)); - expect(within(modal).queryByText('Submit')).not.toBeInTheDocument(); - expect(within(modal).queryByText('Cancel')).not.toBeInTheDocument(); + expect(within(modal).queryByText('Submit')).not.toBeInTheDocument(); + expect(within(modal).queryByText('Cancel')).not.toBeInTheDocument(); - const closeButton = within(modal).getByText('Close'); - expect(closeButton).toBeInTheDocument(); - expect(closeButton).toBeVisible(); + const closeButton = within(modal).getByText('Close'); + expect(closeButton).toBeInTheDocument(); + expect(closeButton).toBeVisible(); + }); }); it('should delete a Probe Template when Delete is clicked', async () => { @@ -197,13 +201,15 @@ describe('', () => { routerConfigs: { routes: [{ path: '/events', element: }] }, }); - await user.click(screen.getByLabelText('Actions')); + await act(async () => { + await user.click(screen.getByLabelText(testT('AgentProbeTemplates.ARIA_LABELS.ROW_ACTION'))); - const deleteButton = await screen.findByText('Delete'); - expect(deleteButton).toBeInTheDocument(); - expect(deleteButton).toBeVisible(); + const deleteButton = await screen.findByText('Delete'); + expect(deleteButton).toBeInTheDocument(); + expect(deleteButton).toBeVisible(); - await user.click(deleteButton); + await user.click(deleteButton); + }); expect(deleteRequestSpy).toHaveBeenCalledTimes(1); expect(deleteRequestSpy).toBeCalledWith('someProbeTemplate'); @@ -215,27 +221,29 @@ describe('', () => { routerConfigs: { routes: [{ path: '/events', element: }] }, }); - await user.click(screen.getByLabelText('Actions')); + await act(async () => { + await user.click(screen.getByLabelText(testT('AgentProbeTemplates.ARIA_LABELS.ROW_ACTION'))); - const deleteButton = await screen.findByText('Delete'); - expect(deleteButton).toBeInTheDocument(); - expect(deleteButton).toBeVisible(); + const deleteButton = await screen.findByText('Delete'); + expect(deleteButton).toBeInTheDocument(); + expect(deleteButton).toBeVisible(); - await user.click(deleteButton); + await user.click(deleteButton); - const warningModal = await screen.findByRole('dialog'); - expect(warningModal).toBeInTheDocument(); - expect(warningModal).toBeVisible(); + const warningModal = await screen.findByRole('dialog'); + expect(warningModal).toBeInTheDocument(); + expect(warningModal).toBeVisible(); - const modalTitle = within(warningModal).getByText(DeleteProbeTemplates.title); - expect(modalTitle).toBeInTheDocument(); - expect(modalTitle).toBeVisible(); + const modalTitle = within(warningModal).getByText(DeleteProbeTemplates.title); + expect(modalTitle).toBeInTheDocument(); + expect(modalTitle).toBeVisible(); - const confirmButton = within(warningModal).getByText('Delete'); - expect(confirmButton).toBeInTheDocument(); - expect(confirmButton).toBeVisible(); + const confirmButton = within(warningModal).getByText('Delete'); + expect(confirmButton).toBeInTheDocument(); + expect(confirmButton).toBeVisible(); - await user.click(confirmButton); + await user.click(confirmButton); + }); expect(deleteRequestSpy).toHaveBeenCalledTimes(1); expect(deleteRequestSpy).toBeCalledWith('someProbeTemplate'); @@ -247,14 +255,16 @@ describe('', () => { routerConfigs: { routes: [{ path: '/events', element: }] }, }); - await user.click(screen.getByLabelText('Actions')); + await act(async () => { + await user.click(screen.getByLabelText(testT('AgentProbeTemplates.ARIA_LABELS.ROW_ACTION'))); - const insertButton = await screen.findByText('Insert probes...'); - expect(insertButton).toBeInTheDocument(); - expect(insertButton).toBeVisible(); - expect(insertButton.getAttribute('aria-disabled')).toBe('false'); + const insertButton = await screen.findByLabelText('insert-template'); + expect(insertButton).toBeInTheDocument(); + expect(insertButton).toBeVisible(); + expect(insertButton.getAttribute('aria-disabled')).toBeNull(); - await user.click(insertButton); + await user.click(insertButton); + }); expect(insertProbesSpy).toHaveBeenCalledTimes(1); expect(insertProbesSpy).toHaveBeenCalledWith(mockProbeTemplate.name); @@ -265,12 +275,14 @@ describe('', () => { routerConfigs: { routes: [{ path: '/events', element: }] }, }); - await user.click(screen.getByLabelText('Actions')); + await act(async () => { + await user.click(screen.getByLabelText(testT('AgentProbeTemplates.ARIA_LABELS.ROW_ACTION'))); - const insertButton = await screen.findByText('Insert probes...'); - expect(insertButton).toBeInTheDocument(); - expect(insertButton).toBeVisible(); - expect(insertButton.getAttribute('aria-disabled')).toBe('true'); + const insertButton = await screen.findByLabelText('insert-template'); + expect(insertButton).toBeInTheDocument(); + expect(insertButton).toBeVisible(); + expect(insertButton.getAttribute('aria-disabled')).toBe('true'); + }); }); it('should shown empty state when table is empty', async () => { @@ -278,7 +290,7 @@ describe('', () => { routerConfigs: { routes: [{ path: '/events', element: }] }, }); - const filterInput = screen.getByLabelText('Probe Template filter'); + const filterInput = screen.getByLabelText(testT('AgentProbeTemplates.ARIA_LABELS.SEARCH_INPUT')); expect(filterInput).toBeInTheDocument(); expect(filterInput).toBeVisible(); diff --git a/src/test/Agent/__snapshots__/AgentLiveProbes.test.tsx.snap b/src/test/Agent/__snapshots__/AgentLiveProbes.test.tsx.snap index 783cd904d..d10e35226 100644 --- a/src/test/Agent/__snapshots__/AgentLiveProbes.test.tsx.snap +++ b/src/test/Agent/__snapshots__/AgentLiveProbes.test.tsx.snap @@ -23,7 +23,7 @@ Array [ } > TypeError: Cannot read properties of null (reading 'offsetWidth') - at /home/thvo/workspace/cryostat-web/node_modules/@patternfly/react-table/dist/js/components/Table/Td.js:136:38 + at /home/thvo/workspace/cryostat-web/node_modules/@patternfly/react-table/dist/js/components/Table/Td.js:140:38 at invokePassiveEffectCreate (/home/thvo/workspace/cryostat-web/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:14504:20) at HTMLUnknownElement.callCallback (/home/thvo/workspace/cryostat-web/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:11391:14) at HTMLUnknownElement.callTheUserObjectsOperation (/home/thvo/workspace/cryostat-web/node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30)