diff --git a/.gitignore b/.gitignore index 729dd9e63..5bf261e7a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ coverage storybook-static .build_cache .eslintcache +.cache* diff --git a/package.json b/package.json index bc18ebc0f..7d9a1639a 100644 --- a/package.json +++ b/package.json @@ -89,14 +89,14 @@ "webpack-merge": "^5.8.0" }, "dependencies": { - "@patternfly/quickstarts": "^2.3.3", - "@patternfly/react-catalog-view-extension": "4.93.15", - "@patternfly/react-charts": "^6.94.18", - "@patternfly/react-core": "4.267.6", - "@patternfly/react-icons": "^4.93.6", - "@patternfly/react-styles": "^4.92.6", - "@patternfly/react-table": "^4.112.39", - "@patternfly/react-topology": "4.91.27", + "@patternfly/quickstarts": "^5.0.0", + "@patternfly/react-catalog-view-extension": "^5.0.0", + "@patternfly/react-charts": "^7.1.1", + "@patternfly/react-core": "^5.1.1", + "@patternfly/react-icons": "^5.1.1", + "@patternfly/react-styles": "^5.1.1", + "@patternfly/react-table": "^5.1.1", + "@patternfly/react-topology": "^5.1.0", "@reduxjs/toolkit": "^1.9.3", "@types/lodash": "^4.14.202", "@types/react": "^17.0.69", diff --git a/src/app/Agent/AgentLiveProbes.tsx b/src/app/Agent/AgentLiveProbes.tsx index f74c6d63c..b10a46980 100644 --- a/src/app/Agent/AgentLiveProbes.tsx +++ b/src/app/Agent/AgentLiveProbes.tsx @@ -34,7 +34,7 @@ import { StackItem, EmptyState, EmptyStateIcon, - Title, + EmptyStateHeader, } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons'; import { @@ -42,7 +42,7 @@ import { ISortBy, SortByDirection, ThProps, - TableComposable, + Table, Tbody, Th, Thead, @@ -176,6 +176,8 @@ export const AgentLiveProbes: React.FC = (_) => { } }, [context.settings, setWarningModalOpen, handleDeleteAllProbes]); + const handleFilterTextChange = React.useCallback((_, value: string) => setFilterText(value), [setFilterText]); + React.useEffect(() => { addSubscription( context.target.target().subscribe(() => { @@ -329,7 +331,7 @@ export const AgentLiveProbes: React.FC = (_) => { type="search" placeholder="Filter..." aria-label="Active probe filter" - onChange={setFilterText} + onChange={handleFilterTextChange} /> @@ -355,7 +357,7 @@ export const AgentLiveProbes: React.FC = (_) => { /> {probeRows.length ? ( - + {tableColumns.map(({ title, sortable }, index) => ( @@ -366,13 +368,14 @@ export const AgentLiveProbes: React.FC = (_) => { {probeRows} - +
) : ( - - - No active probes - + } + headingLevel="h4" + /> )} diff --git a/src/app/Agent/AgentProbeTemplates.tsx b/src/app/Agent/AgentProbeTemplates.tsx index d09b4eb18..03f53c85a 100644 --- a/src/app/Agent/AgentProbeTemplates.tsx +++ b/src/app/Agent/AgentProbeTemplates.tsx @@ -26,30 +26,31 @@ import { TableColumn, portalRoot, sortResources } from '@app/utils/utils'; import { ActionGroup, Button, - Dropdown, - DropdownItem, - DropdownPosition, EmptyState, EmptyStateIcon, Form, FormGroup, - KebabToggle, Modal, ModalVariant, Stack, StackItem, TextInput, - Title, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, + EmptyStateHeader, + Dropdown, + DropdownItem, + DropdownList, + MenuToggleElement, + MenuToggle, } from '@patternfly/react-core'; -import { SearchIcon, UploadIcon } from '@patternfly/react-icons'; +import { SearchIcon, EllipsisVIcon, UploadIcon } from '@patternfly/react-icons'; import { ISortBy, SortByDirection, - TableComposable, + Table, TableVariant, Tbody, Td, @@ -165,6 +166,8 @@ export const AgentProbeTemplates: React.FC = ({ agentD setUploadModalOpen(false); }, [setUploadModalOpen]); + const handleFilterTextChange = React.useCallback((_, value: string) => setFilterText(value), [setFilterText]); + React.useEffect(() => { refreshTemplates(); }, [refreshTemplates]); @@ -302,7 +305,7 @@ export const AgentProbeTemplates: React.FC = ({ agentD type="search" placeholder="Filter..." aria-label="Probe Template filter" - onChange={setFilterText} + onChange={handleFilterTextChange} value={filterText} /> @@ -323,7 +326,7 @@ export const AgentProbeTemplates: React.FC = ({ agentD {templateRows.length ? ( - + {tableColumns.map(({ title, sortable }, index) => ( @@ -334,13 +337,14 @@ export const AgentProbeTemplates: React.FC = ({ agentD {...templateRows} - +
) : ( - - - No Probe Templates - + } + headingLevel="h4" + /> )} @@ -458,6 +462,9 @@ export const AgentProbeTemplateUploadModal: React.FC = ({ onInse ]; }, [onInsert, onDelete, template]); - return ( - } - menuAppendTo={document.body} - position={DropdownPosition.right} - isFlipEnabled - dropdownItems={actionItems.map((action) => ( + const handleToggle = React.useCallback((_, opened: boolean) => setIsOpen(opened), [setIsOpen]); + + const dropdownItems = React.useMemo( + () => + actionItems.map((action) => ( { @@ -533,7 +536,26 @@ export const AgentTemplateAction: React.FC = ({ onInse > {action.title} - ))} - /> + )), + [actionItems, setIsOpen], + ); + + return ( + ) => ( + handleToggle(event, !isOpen)}> + + + )} + isOpen={isOpen} + popperProps={{ + appendTo: document.body, + position: 'right', + enableFlip: true, + }} + > + {dropdownItems} + ); }; diff --git a/src/app/AppLayout/AppLayout.tsx b/src/app/AppLayout/AppLayout.tsx index cf7d5bbe3..e166a0fdb 100644 --- a/src/app/AppLayout/AppLayout.tsx +++ b/src/app/AppLayout/AppLayout.tsx @@ -39,14 +39,8 @@ import { AlertActionCloseButton, AlertGroup, AlertVariant, - ApplicationLauncher, - ApplicationLauncherItem, Brand, Button, - Dropdown, - DropdownGroup, - DropdownItem, - DropdownToggle, Icon, Label, Masthead, @@ -66,12 +60,20 @@ import { ToolbarContent, ToolbarGroup, ToolbarItem, + PageSidebarBody, + MenuToggleElement, + MenuToggle, + DropdownList, + DropdownGroup, + DropdownItem, + Dropdown, } from '@patternfly/react-core'; import { BarsIcon, BellIcon, CaretDownIcon, CogIcon, + EllipsisVIcon, ExternalLinkAltIcon, PlusCircleIcon, QuestionCircleIcon, @@ -231,7 +233,7 @@ export const AppLayout: React.FC = ({ children }) => { // prevent page resize to close nav during tour const onPageResize = React.useCallback( - (props: { mobileView: boolean; windowSize: number }) => { + (_, props: { mobileView: boolean; windowSize: number }) => { if (joyState.run === false) { setIsMobileView(props.mobileView); setIsNavOpen(!props.mobileView); @@ -293,11 +295,16 @@ export const AppLayout: React.FC = ({ children }) => { [handleLogout, handleLanguagePref], ); - const UserInfoToggle = React.useMemo( - () => ( - - {username || } - + const UserInfoToggle = React.useCallback( + (toggleRef: React.Ref) => ( + + {username || ( + + + + )} + + ), [username, handleUserInfoToggle], ); @@ -326,28 +333,27 @@ export const AppLayout: React.FC = ({ children }) => { const helpItems = React.useMemo(() => { return [ - {t('AppLayout.APP_LAUNCHER.QUICKSTARTS')}} - />, - + + {t('AppLayout.APP_LAUNCHER.QUICKSTARTS')} + , + {t('AppLayout.APP_LAUNCHER.DOCUMENTATION')} - , - + , + {t('AppLayout.APP_LAUNCHER.GUIDED_TOUR')} - , - + , + {t('AppLayout.APP_LAUNCHER.HELP')} - , - + , + {t('AppLayout.APP_LAUNCHER.ABOUT')} - , + , ]; }, [t, handleOpenDocumentation, handleOpenGuidedTour, handleOpenDiscussion, handleOpenAboutModal]); @@ -363,18 +369,22 @@ export const AppLayout: React.FC = ({ children }) => { ); }, []); - const HeaderToolbar = React.useMemo( + const headerToolbar = React.useMemo( () => ( <> - + - handleHelpToggle()} + className="application-launcher" + toggle={(toggleRef: React.Ref) => ( + } + className="application-launcher" + onClick={() => handleHelpToggle()} + > + + + )} isOpen={showHelpDropdown} - items={helpItems} - position="right" - toggleIcon={} - data-tour-id="application-launcher" - data-quickstart-id="application-launcher" - /> + popperProps={{ + position: 'right', + }} + > + {helpItems} + setShowUserInfoDropdown(false)} - isOpen={showUserInfoDropdown} toggle={UserInfoToggle} - position="right" - dropdownItems={userInfoItems} - /> + isOpen={showUserInfoDropdown} + popperProps={{ + position: 'right', + }} + > + {userInfoItems} + @@ -445,7 +470,7 @@ export const AppLayout: React.FC = ({ children }) => { ], ); - const Header = React.useMemo( + const header = React.useMemo( () => ( <> @@ -453,8 +478,8 @@ export const AppLayout: React.FC = ({ children }) => { @@ -467,15 +492,14 @@ export const AppLayout: React.FC = ({ children }) => { - - {HeaderToolbar} + {headerToolbar} ), - [isNavOpen, aboutModalOpen, HeaderToolbar, handleCloseAboutModal, onNavToggle, levelBadge], + [isNavOpen, aboutModalOpen, headerToolbar, handleCloseAboutModal, onNavToggle, levelBadge], ); const isActiveRoute = React.useCallback( @@ -535,7 +559,11 @@ export const AppLayout: React.FC = ({ children }) => { ); const Sidebar = React.useMemo( - () => , + () => ( + + {Navigation} + + ), [Navigation, isNavOpen], ); @@ -582,7 +610,7 @@ export const AppLayout: React.FC = ({ children }) => { = ({ isRequired type="text" id="username" - onChange={(v) => { + onChange={(_event, v) => { setUsername(v); onCredentialChange && onCredentialChange({ @@ -102,7 +102,7 @@ export const CredentialAuthForm: React.FC = ({ isRequired type="password" id="password" - onChange={(v) => { + onChange={(_event, v) => { setPassword(v); onCredentialChange && onCredentialChange({ diff --git a/src/app/AppLayout/NotificationCenter.tsx b/src/app/AppLayout/NotificationCenter.tsx index 4e1c0812a..f0e5048a9 100644 --- a/src/app/AppLayout/NotificationCenter.tsx +++ b/src/app/AppLayout/NotificationCenter.tsx @@ -20,8 +20,9 @@ import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { Dropdown, DropdownItem, - DropdownPosition, - KebabToggle, + DropdownList, + MenuToggle, + MenuToggleElement, NotificationDrawer, NotificationDrawerBody, NotificationDrawerGroup, @@ -34,6 +35,7 @@ import { Text, TextVariants, } from '@patternfly/react-core'; +import { EllipsisVIcon } from '@patternfly/react-icons'; import * as React from 'react'; import { combineLatest } from 'rxjs'; @@ -129,14 +131,17 @@ export const NotificationCenter: React.FC = ({ onClose return dayjs(timestamp).tz(datetimeContext.timeZone.full).format('L LTS z'); }; - const drawerDropdownItems = [ - - Mark all read - , - - Clear all - , - ]; + const drawerDropdownItems = React.useMemo( + () => [ + + Mark all read + , + + Clear all + , + ], + [handleMarkAllRead, handleClearAll], + ); return ( <> @@ -145,11 +150,23 @@ export const NotificationCenter: React.FC = ({ onClose } + toggle={(toggleRef: React.Ref) => ( + setHeaderDropdownOpen((open) => !open)} + isExpanded={isHeaderDropdownOpen} + > + + + )} isOpen={isHeaderDropdownOpen} - position={DropdownPosition.right} - dropdownItems={drawerDropdownItems} - /> + popperProps={{ + position: 'right', + }} + > + {drawerDropdownItems} + diff --git a/src/app/Archives/AllArchivedRecordingsTable.tsx b/src/app/Archives/AllArchivedRecordingsTable.tsx index d07ed738a..e5786df73 100644 --- a/src/app/Archives/AllArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllArchivedRecordingsTable.tsx @@ -32,22 +32,13 @@ import { EmptyState, EmptyStateIcon, Text, - Title, Tooltip, Split, SplitItem, + EmptyStateHeader, } from '@patternfly/react-core'; import { HelpIcon, SearchIcon } from '@patternfly/react-icons'; -import { - TableComposable, - Th, - Thead, - Tbody, - Tr, - Td, - ExpandableRowContent, - SortByDirection, -} from '@patternfly/react-table'; +import { Table, Th, Thead, Tbody, Tr, Td, ExpandableRowContent, SortByDirection } from '@patternfly/react-table'; import * as React from 'react'; import { Observable, of } from 'rxjs'; import { getTargetFromDirectory, includesDirectory, indexOfDirectory } from './utils'; @@ -112,7 +103,7 @@ export const AllArchivedRecordingsTable: React.FC { + (_, searchInput: string) => { setSearchText(searchInput); }, [setSearchText], @@ -316,17 +307,18 @@ export const AllArchivedRecordingsTable: React.FC - - - No Archived Recordings - + } + headingLevel="h4" + /> ); } else { view = ( <> - + {rowPairs} - +
@@ -342,7 +334,7 @@ export const AllArchivedRecordingsTable: React.FC
); } diff --git a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx index 1eb9d579f..c276298bd 100644 --- a/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx +++ b/src/app/Archives/AllTargetsArchivedRecordingsTable.tsx @@ -33,19 +33,10 @@ import { Checkbox, EmptyState, EmptyStateIcon, - Title, + EmptyStateHeader, } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons'; -import { - TableComposable, - Th, - Thead, - Tbody, - Tr, - Td, - ExpandableRowContent, - SortByDirection, -} from '@patternfly/react-table'; +import { Table, Th, Thead, Tbody, Tr, Td, ExpandableRowContent, SortByDirection } from '@patternfly/react-table'; import * as React from 'react'; import { Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -309,12 +300,17 @@ export const AllTargetsArchivedRecordingsTable: React.FC { + (_, searchInput: string) => { setSearchText(searchInput); }, [setSearchText], ); + const handleHideEmptyTarget = React.useCallback( + (_, hide: boolean) => setHideEmptyTargets(hide), + [setHideEmptyTargets], + ); + const handleSearchInputClear = React.useCallback(() => { setSearchText(''); }, [setSearchText]); @@ -482,17 +478,18 @@ export const AllTargetsArchivedRecordingsTable: React.FC - - - No Archived Recordings - + } + headingLevel="h4" + /> ); } else { view = ( <> - + {rowPairs} - +
@@ -508,7 +505,7 @@ export const AllTargetsArchivedRecordingsTable: React.FC
); } @@ -532,7 +529,7 @@ export const AllTargetsArchivedRecordingsTable: React.FC = ({ onClose, submitRef={submitRef} abortRef={abortRef} uploading={uploading} + dropZoneAccepts={{ + 'application/octet-stream': ['.jfr'], + }} displayAccepts={['JFR']} onFileSubmit={onFileSubmit} onFilesChange={onFilesChange} @@ -194,7 +198,9 @@ export const ArchiveUploadModal: React.FC = ({ onClose, content={Unique key-value pairs containing information about the recording.} appendTo={portalRoot} > - + + + } > diff --git a/src/app/Archives/Archives.tsx b/src/app/Archives/Archives.tsx index b81e6954a..de30b50ca 100644 --- a/src/app/Archives/Archives.tsx +++ b/src/app/Archives/Archives.tsx @@ -19,7 +19,16 @@ import { Target, UPLOADS_SUBDIRECTORY } from '@app/Shared/Services/api.types'; import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { getActiveTab, switchTab } from '@app/utils/utils'; -import { Card, CardBody, EmptyState, EmptyStateIcon, Tab, Tabs, TabTitleText, Title } from '@patternfly/react-core'; +import { + Card, + CardBody, + EmptyState, + EmptyStateIcon, + Tab, + Tabs, + TabTitleText, + EmptyStateHeader, +} from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons'; import * as React from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; @@ -90,10 +99,11 @@ export const Archives: React.FC = ({ ...props }) => { ) : ( - - - Archives Unavailable - + } + headingLevel="h4" + /> ); }, [archiveEnabled, activeTab, uploadTargetAsObs, onTabSelect]); diff --git a/src/app/CreateRecording/CustomRecordingForm.tsx b/src/app/CreateRecording/CustomRecordingForm.tsx index f09faec1e..afb6b5856 100644 --- a/src/app/CreateRecording/CustomRecordingForm.tsx +++ b/src/app/CreateRecording/CustomRecordingForm.tsx @@ -38,10 +38,12 @@ import { ExpandableSection, Form, FormGroup, + FormHelperText, FormSelect, FormSelectOption, HelperText, HelperTextItem, + Icon, Label, Split, SplitItem, @@ -108,12 +110,12 @@ export const CustomRecordingForm: React.FC = () => { ); const handleRestartExistingChange = React.useCallback( - (checked: boolean) => setFormData((old) => ({ ...old, restart: checked })), + (_, checked: boolean) => setFormData((old) => ({ ...old, restart: checked })), [setFormData], ); const handleContinuousChange = React.useCallback( - (checked: boolean) => + (_, checked: boolean) => setFormData((old) => ({ ...old, continuous: checked, @@ -156,7 +158,7 @@ export const CustomRecordingForm: React.FC = () => { }, [formData]); const handleRecordingNameChange = React.useCallback( - (name: string) => + (_, name: string) => setFormData((old) => ({ ...old, name: name, @@ -166,27 +168,27 @@ export const CustomRecordingForm: React.FC = () => { ); const handleMaxAgeChange = React.useCallback( - (value: string) => setFormData((old) => ({ ...old, maxAge: Number(value) })), + (_, value: string) => setFormData((old) => ({ ...old, maxAge: Number(value) })), [setFormData], ); const handleMaxAgeUnitChange = React.useCallback( - (unit: string) => setFormData((old) => ({ ...old, maxAgeUnit: Number(unit) })), + (_, unit: string) => setFormData((old) => ({ ...old, maxAgeUnit: Number(unit) })), [setFormData], ); const handleMaxSizeChange = React.useCallback( - (value: string) => setFormData((old) => ({ ...old, maxSize: Number(value) })), + (_, value: string) => setFormData((old) => ({ ...old, maxSize: Number(value) })), [setFormData], ); const handleMaxSizeUnitChange = React.useCallback( - (unit: string) => setFormData((old) => ({ ...old, maxSizeUnit: Number(unit) })), + (_, unit: string) => setFormData((old) => ({ ...old, maxSizeUnit: Number(unit) })), [setFormData], ); const handleToDiskChange = React.useCallback( - (toDisk: boolean) => setFormData((old) => ({ ...old, toDisk })), + (_, toDisk: boolean) => setFormData((old) => ({ ...old, toDisk })), [setFormData], ); @@ -203,7 +205,7 @@ export const CustomRecordingForm: React.FC = () => { ); const handleArchiveOnStopChange = React.useCallback( - (archiveOnStop: boolean) => setFormData((old) => ({ ...old, archiveOnStop })), + (_, archiveOnStop: boolean) => setFormData((old) => ({ ...old, archiveOnStop })), [setFormData], ); @@ -406,14 +408,7 @@ export const CustomRecordingForm: React.FC = () => { are built in to the JVM itself, while others are user defined.
- + { id="recording-restart-existing" name="recording-restart-existing" /> + + + + {formData.nameValid === ValidatedOptions.error + ? 'A Recording name can contain only letters, numbers, and underscores.' + : 'Enter a Recording name. This will be unique within the target JVM.'} + + + - + { unitScalar={formData.durationUnit} onUnitScalarChange={handleDurationUnitChange} /> + + + + {formData.durationValid === ValidatedOptions.error + ? 'The Recording duration must be a positive integer.' + : formData.continuous + ? 'A continuous recording will never be automatically stopped.' + : formData.archiveOnStop + ? 'Time before the Recording is automatically stopped and copied to archive.' + : 'Time before the Recording is automatically stopped.'} + + + - + { disabled={loading} onSelect={handleTemplateChange} /> + + + + {formData.template?.name + ? 'The Event Template to be applied in this Recording' + : 'A Template must be selected'} + + + { content={Unique key-value pairs containing information about the Recording.} appendTo={portalRoot} > - + + + } - isHelperTextBeforeField - helperText={ + > + { set by Cryostat and will be overwritten if specifed. - } - > + { data-quickstart-id="crf-advanced-opt" > A value of 0 for maximum size or age means unbounded. - + { onChange={handleToDiskChange} isDisabled={loading} /> + + + + Write contents of buffer onto disk. If disabled, the buffer acts as circular buffer only keeping the most recent Recording information + + + - + { + + + The maximum size of Recording data saved to disk + + - + { + + + The maximum age of Recording data stored to disk + + diff --git a/src/app/Dashboard/AddCard.tsx b/src/app/Dashboard/AddCard.tsx index cf073eba1..cd186d39d 100644 --- a/src/app/Dashboard/AddCard.tsx +++ b/src/app/Dashboard/AddCard.tsx @@ -52,9 +52,6 @@ import { LevelItem, Modal, NumberInput, - Select, - SelectOption, - SelectOptionObject, Stack, StackItem, Switch, @@ -63,16 +60,24 @@ import { TextInput, Title, Tooltip, -} from '@patternfly/react-core'; -import { - CustomWizardNavFunction, Wizard, - WizardControlStep, WizardHeader, WizardNav, - WizardNavItem, WizardStep, -} from '@patternfly/react-core/dist/js/next'; + WizardNavItem, + EmptyStateHeader, + EmptyStateFooter, + FormHelperText, + HelperText, + HelperTextItem, + WizardStepType, + CustomWizardNavFunction, + Select, + SelectOption, + SelectList, + MenuToggle, + MenuToggleElement, +} from '@patternfly/react-core'; import { PlusCircleIcon } from '@patternfly/react-icons'; import { TFunction } from 'i18next'; import { nanoid } from 'nanoid'; @@ -84,7 +89,7 @@ import { ChartContext } from './Charts/context'; import { CardConfig, DashboardCardDescriptor, PropControl } from './types'; import { getCardDescriptorByTitle, getDashboardCards } from './utils'; -interface AddCardProps { +export interface AddCardProps { variant: 'card' | 'icon-button'; } @@ -137,23 +142,23 @@ export const AddCard: React.FC = ({ variant }) => { const customNav: CustomWizardNavFunction = React.useCallback( ( isExpanded: boolean, - steps: WizardControlStep[], - activeStep: WizardControlStep, + steps: WizardStepType[], + activeStep: WizardStepType, goToStepByIndex: (index: number) => void, ) => { return ( {steps .filter((step) => !step.isHidden) - .map((step, idx) => ( + .map((step) => ( 0 && !selection)} + isDisabled={step.isDisabled || (step.index > 0 && !selection)} stepIndex={step.index} - onNavItemClick={goToStepByIndex} + onClick={() => goToStepByIndex(step.index)} /> ))} @@ -169,15 +174,18 @@ export const AddCard: React.FC = ({ variant }) => { - - - - Add a new card - + + } + headingLevel="h2" + /> {t('Dashboard.CARD_CATALOG_DESCRIPTION')} - + + + @@ -297,7 +305,10 @@ const getFullDescription = (selection: string, t: TFunction) => { export interface CardGalleryProps { selection: string; // Translated card title - onSelect: (event: React.MouseEvent, selection: string) => void; + onSelect: ( + event: React.MouseEvent | React.FormEvent, + selection: string, + ) => void; } export const CardGallery: React.FC = ({ selection, onSelect }) => { @@ -314,20 +325,25 @@ export const CardGallery: React.FC = ({ selection, onSelect }) { - if (selection === t(title)) { - setToViewCard(availableCards.find((card) => t(card.title) === selection)); - } else { - onSelect(event, t(title)); - } - }} + isClickable + isSelectable isFullHeight isFlat isSelected={selection === t(title)} + tabIndex={0} > - + { + if (selection === t(title)) { + setToViewCard(availableCards.find((card) => t(card.title) === selection)); + } else { + onSelect(event, t(title)); + } + }, + selectableActionId: title, + }} + > {icon ? {icon} : null} @@ -527,7 +543,12 @@ const PropsConfigForm: React.FC = ({ onChange, ...props }) break; } return ( - + + + + {t(ctrl.description)} + + {input} ); @@ -552,10 +573,11 @@ const PropsConfigForm: React.FC = ({ onChange, ...props }) interface SelectControlProps { handleChange: (selection: string) => void; control: PropControl; - selectedConfig: string | SelectOptionObject; + selectedConfig: string; + isDisabled?: boolean; } -const SelectControl: React.FC = ({ handleChange, control, selectedConfig }) => { +const SelectControl: React.FC = ({ handleChange, control, selectedConfig, isDisabled }) => { const addSubscription = useSubscriptions(); const [selectOpen, setSelectOpen] = React.useState(false); @@ -563,15 +585,15 @@ const SelectControl: React.FC = ({ handleChange, control, se const [errored, setErrored] = React.useState(false); const handleSelect = React.useCallback( - (_, selection, isPlaceholder) => { - if (!isPlaceholder) { - handleChange(selection); - } + (_, selection) => { + handleChange(selection); setSelectOpen(false); }, [handleChange, setSelectOpen], ); + const handleToggle = React.useCallback((_) => setSelectOpen((isOpen) => !isOpen), [setSelectOpen]); + React.useEffect(() => { let obs; if (control.values instanceof Observable) { @@ -598,27 +620,40 @@ const SelectControl: React.FC = ({ handleChange, control, se ); }, [addSubscription, setOptions, setErrored, control, control.values]); + const toggle = React.useCallback( + (toggleRef: React.Ref) => ( + + {selectedConfig} + + ), + [handleToggle, isDisabled, selectOpen, selectedConfig], + ); + return ( ); }; diff --git a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx index 51cfe0639..d807e2bf6 100644 --- a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx +++ b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx @@ -53,7 +53,6 @@ import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { calculateAnalysisTimer, portalRoot } from '@app/utils/utils'; import { Button, - CardActions, CardBody, CardExpandableContent, CardHeader, @@ -62,7 +61,6 @@ import { EmptyState, EmptyStateBody, EmptyStateIcon, - EmptyStateSecondaryActions, Grid, GridItem, Label, @@ -75,12 +73,14 @@ import { Text, TextContent, TextVariants, - Title, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, Tooltip, + EmptyStateActions, + EmptyStateHeader, + EmptyStateFooter, } from '@patternfly/react-core'; import { CheckCircleIcon, @@ -598,22 +598,25 @@ export const AutomatedAnalysisCard: DashboardCardFC if (filtered.length === 0) { return ( - - - {t(`AutomatedAnalysisCard.NO_RESULTS`)} - + {t(`AutomatedAnalysisCard.NO_RESULTS`)}} + icon={} + headingLevel="h4" + /> {t('AutomatedAnalysisCard.NO_RESULTS_BODY')} - - - - - + + + + + + + ); } @@ -670,7 +673,7 @@ export const AutomatedAnalysisCard: DashboardCardFC - + +
+ +
+
); }, [t, onCogSelect, onDefaultRecordingStart]); diff --git a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.tsx b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.tsx index 3a10c0ebd..f74402f2d 100644 --- a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.tsx +++ b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisConfigForm.tsx @@ -26,7 +26,6 @@ import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { Button, Card, - CardActions, CardBody, CardHeader, CardTitle, @@ -36,6 +35,7 @@ import { DescriptionListTerm, Form, FormGroup, + FormHelperText, FormSection, FormSelect, FormSelectOption, @@ -248,25 +248,7 @@ export const AutomatedAnalysisConfigForm: React.FC - - - {t('AutomatedAnalysisConfigForm.TEMPLATE_HELPER_TEXT')} - {formData.template?.type == 'TARGET' && errorMessage === '' && ( - - - {t('AutomatedAnalysisConfigForm.TEMPLATE_INVALID_WARNING')} - - - )} - - - } - > + {isLoading ? ( ) : errorMessage != '' ? ( @@ -282,14 +264,22 @@ export const AutomatedAnalysisConfigForm: React.FC )} + + + {t('AutomatedAnalysisConfigForm.TEMPLATE_HELPER_TEXT')} + {formData.template?.type == 'TARGET' && errorMessage === '' && ( + + + {t('AutomatedAnalysisConfigForm.TEMPLATE_INVALID_WARNING')} + + + )} + + - + + + + {t('MAXIMUM_SIZE_HELPER_TEXT', { ns: 'common' })} + + - + + + + + {t('MAXIMUM_AGE_HELPER_TEXT', { ns: 'common' })} + + @@ -419,30 +416,37 @@ export const AutomatedAnalysisConfigForm: React.FC ( <> - + + {editing && ( + + )} + + + ), + hasNoOffset: false, + className: undefined, + }} + > {t('AutomatedAnalysisConfigForm.CURRENT_CONFIG')} - - {editing && ( - - )} - - diff --git a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisFilters.tsx b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisFilters.tsx index 6983c1167..63b907fcc 100644 --- a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisFilters.tsx +++ b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisFilters.tsx @@ -18,17 +18,18 @@ import { UpdateFilterOptions } from '@app/Shared/Redux/Filters/Common'; import { automatedAnalysisUpdateCategoryIntent, RootState, StateDispatch } from '@app/Shared/Redux/ReduxStore'; import { AnalysisResult } from '@app/Shared/Services/api.types'; import { - Dropdown, - DropdownItem, - DropdownPosition, - DropdownToggle, ToolbarChipGroup, ToolbarFilter, ToolbarGroup, ToolbarItem, ToolbarToggleGroup, + Dropdown, + DropdownItem, + DropdownList, + MenuToggle, + MenuToggleElement, } from '@patternfly/react-core'; -import { FilterIcon } from '@patternfly/react-icons'; +import { EllipsisVIcon, FilterIcon } from '@patternfly/react-icons'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; @@ -126,20 +127,26 @@ export const AutomatedAnalysisFilters: React.FC = const categoryDropdown = React.useMemo(() => { return ( - {getCategoryDisplay(currentCategory)} - - } + toggle={(toggleRef: React.Ref) => ( + onCategoryToggle()}> + + {getCategoryDisplay(currentCategory)} + + + )} isOpen={isCategoryDropdownOpen} - dropdownItems={allowedAutomatedAnalysisFilters.map((cat) => ( - onCategorySelect(cat)}> - {cat} - - ))} - /> + popperProps={{ + position: 'left', + }} + > + + {allowedAutomatedAnalysisFilters.map((cat) => ( + onCategorySelect(cat)}> + {cat} + + ))} + + ); }, [isCategoryDropdownOpen, currentCategory, onCategoryToggle, onCategorySelect, getCategoryDisplay]); diff --git a/src/app/Dashboard/AutomatedAnalysis/ClickableAutomatedAnalysisLabel.tsx b/src/app/Dashboard/AutomatedAnalysis/ClickableAutomatedAnalysisLabel.tsx index 9f00adc29..e55dce124 100644 --- a/src/app/Dashboard/AutomatedAnalysis/ClickableAutomatedAnalysisLabel.tsx +++ b/src/app/Dashboard/AutomatedAnalysis/ClickableAutomatedAnalysisLabel.tsx @@ -40,7 +40,6 @@ export const ClickableAutomatedAnalysisLabel: React.FC setIsHoveredOrFocused(false), [setIsHoveredOrFocused]); const alertStyle = { - default: popoverStyles.modifiers.default, info: popoverStyles.modifiers.info, success: popoverStyles.modifiers.success, warning: popoverStyles.modifiers.warning, @@ -61,7 +60,7 @@ export const ClickableAutomatedAnalysisLabel: React.FC { return result.score == AutomatedAnalysisScore.NA_SCORE - ? 'default' + ? 'info' : result.score < AutomatedAnalysisScore.ORANGE_SCORE_THRESHOLD ? 'success' : result.score < AutomatedAnalysisScore.RED_SCORE_THRESHOLD @@ -83,7 +82,7 @@ export const ClickableAutomatedAnalysisLabel: React.FC{result.name}} alertSeverityVariant={alertPopoverVariant} diff --git a/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisNameFilter.tsx b/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisNameFilter.tsx index 50569ecc0..693be9c0e 100644 --- a/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisNameFilter.tsx +++ b/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisNameFilter.tsx @@ -16,7 +16,19 @@ import { CategorizedRuleEvaluations } from '@app/Shared/Services/api.types'; import { portalRoot } from '@app/utils/utils'; -import { Select, SelectOption, SelectVariant } from '@patternfly/react-core'; +import { + Button, + MenuToggle, + MenuToggleElement, + Select, + SelectList, + SelectOption, + SelectOptionProps, + TextInputGroup, + TextInputGroupMain, + TextInputGroupUtilities, +} from '@patternfly/react-core'; +import { TimesIcon } from '@patternfly/react-icons'; import * as React from 'react'; export interface AutomatedAnalysisNameFilterProps { @@ -25,12 +37,17 @@ export interface AutomatedAnalysisNameFilterProps { onSubmit: (inputName: string) => void; } -export const AutomatedAnalysisNameFilter: React.FC = ({ onSubmit, ...props }) => { +export const AutomatedAnalysisNameFilter: React.FC = ({ + onSubmit, + filteredNames, + evaluations, +}) => { const [isExpanded, setIsExpanded] = React.useState(false); + const [filterValue, setFilterValue] = React.useState(''); const onSelect = React.useCallback( - (_, selection, isPlaceholder) => { - if (!isPlaceholder) { + (_, selection) => { + if (selection) { setIsExpanded(false); onSubmit(selection); } @@ -38,32 +55,83 @@ export const AutomatedAnalysisNameFilter: React.FC setIsExpanded((isExpanded) => !isExpanded), [setIsExpanded]); + + const onInputChange = React.useCallback((_, inputVal: string) => setFilterValue(inputVal), [setFilterValue]); + const nameOptions = React.useMemo(() => { const flatEvalMap: string[] = [] as string[]; - for (const topic of props.evaluations.map((r) => r[1])) { + for (const topic of evaluations.map((r) => r[1])) { for (const rule of topic) { flatEvalMap.push(rule.name); } } - return flatEvalMap - .filter((n) => !props.filteredNames.includes(n)) - .sort() - .map((option, index) => ); - }, [props.evaluations, props.filteredNames]); + return flatEvalMap.filter((n) => !filteredNames.includes(n)).sort(); + }, [evaluations, filteredNames]); + + const filteredNameOptions = React.useMemo(() => { + return !filterValue ? nameOptions : nameOptions.filter((n) => n.includes(filterValue.toLowerCase())); + }, [filterValue, nameOptions]); + + const selectOptionProps: SelectOptionProps[] = React.useMemo(() => { + if (!filteredNameOptions.length) { + return [{ isDisabled: true, children: `No results found for "${filterValue}"`, value: undefined }]; + } + return filteredNameOptions.map((n) => ({ children: n, value: n })); + }, [filteredNameOptions, filterValue]); + + const toggle = React.useCallback( + (toggleRef: React.Ref) => ( + + + + + {filterValue ? ( + + ) : null} + + + + ), + [onToggle, isExpanded, filterValue, onInputChange, setFilterValue], + ); return ( ); }; diff --git a/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisScoreFilter.tsx b/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisScoreFilter.tsx index 3707ccc87..a0e93cde9 100644 --- a/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisScoreFilter.tsx +++ b/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisScoreFilter.tsx @@ -64,7 +64,7 @@ export const AutomatedAnalysisScoreFilter: React.FC { + (_, value: number, inputValue: number | undefined) => { value = Math.round(value * 10) / 10; let newValue; if (inputValue === undefined) { @@ -101,14 +101,14 @@ export const AutomatedAnalysisScoreFilter: React.FC {t('RESET', { ns: 'common' })}: + ) : null} + + + + ), + [onToggle, isExpanded, filterValue, onInputChange, setFilterValue], + ); + return ( ); }; diff --git a/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx b/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx index a6f4bc17d..739fea348 100644 --- a/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx +++ b/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx @@ -28,7 +28,6 @@ import { useTheme } from '@app/utils/hooks/useTheme'; import { Bullseye, Button, - CardActions, CardBody, CardHeader, CardTitle, @@ -37,7 +36,8 @@ import { EmptyStateIcon, EmptyStateVariant, Label, - Title, + EmptyStateHeader, + EmptyStateFooter, } from '@patternfly/react-core'; import { DataSourceIcon, ExternalLinkAltIcon, SyncAltIcon, TachometerAltIcon } from '@patternfly/react-icons'; import * as React from 'react'; @@ -87,7 +87,7 @@ export function kindToId(kind: string): number { } export const JFRMetricsChartCard: DashboardCardFC = (props) => { - const [t] = useTranslation(); + const { t } = useTranslation(); const serviceContext = React.useContext(ServiceContext); const controllerContext = React.useContext(ChartContext); const navigate = useNavigate(); @@ -193,11 +193,16 @@ export const JFRMetricsChartCard: DashboardCardFC = (p const header = React.useMemo(() => { return ( - + {controllerState === ControllerState.READY ? actions : props.actions}, + hasNoOffset: false, + className: undefined, + }} + > {t('CHART_CARD.TITLE', { chartKind: props.chartKind, duration: props.duration, period: props.period })} - {controllerState === ControllerState.READY ? actions : props.actions} ); }, [props.actions, props.chartKind, props.duration, props.period, t, controllerState, actions]); @@ -246,11 +251,12 @@ export const JFRMetricsChartCard: DashboardCardFC = (p ) : ( - - - - {t('CHART_CARD.NO_RECORDING.TITLE')} - + + {t('CHART_CARD.NO_RECORDING.TITLE')}} + icon={} + headingLevel="h2" + /> = (p CHART_CARD.NO_RECORDING.DESCRIPTION - + + + )} diff --git a/src/app/Dashboard/Charts/mbean/MBeanMetricsChartCard.tsx b/src/app/Dashboard/Charts/mbean/MBeanMetricsChartCard.tsx index 016742443..eace6bedc 100644 --- a/src/app/Dashboard/Charts/mbean/MBeanMetricsChartCard.tsx +++ b/src/app/Dashboard/Charts/mbean/MBeanMetricsChartCard.tsx @@ -37,9 +37,8 @@ import { ChartLegend, ChartLine, ChartVoronoiContainer, - getResizeObserver, } from '@patternfly/react-charts'; -import { Button, CardActions, CardBody, CardHeader, CardTitle } from '@patternfly/react-core'; +import { getResizeObserver, Button, CardBody, CardHeader, CardTitle } from '@patternfly/react-core'; import { MonitoringIcon, SyncAltIcon } from '@patternfly/react-icons'; import _ from 'lodash'; import * as React from 'react'; @@ -445,11 +444,10 @@ export const MBeanMetricsChartCard: DashboardCardFC const header = React.useMemo( () => ( - + {actions}, hasNoOffset: false, className: undefined }}> {t('CHART_CARD.TITLE', { chartKind: props.chartKind, duration: props.duration, period: props.period })} - {actions} ), [t, props.chartKind, props.duration, props.period, actions], diff --git a/src/app/Dashboard/DashboardCardActionMenu.tsx b/src/app/Dashboard/DashboardCardActionMenu.tsx index 68b9d62ce..c45661d56 100644 --- a/src/app/Dashboard/DashboardCardActionMenu.tsx +++ b/src/app/Dashboard/DashboardCardActionMenu.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ import { portalRoot } from '@app/utils/utils'; -import { Dropdown, DropdownItem, KebabToggle } from '@patternfly/react-core'; +import { Dropdown, DropdownItem, DropdownList, MenuToggle, MenuToggleElement } from '@patternfly/react-core'; +import { EllipsisVIcon } from '@patternfly/react-icons'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; @@ -24,46 +25,58 @@ export interface DashboardCardActionProps { onResetSize: () => void; } -export const DashboardCardActionMenu: React.FC = ({ - onRemove, - onResetSize, - onView, - ...props -}) => { - const [isOpen, setOpen] = React.useState(false); +export const DashboardCardActionMenu: React.FC = ({ onRemove, onResetSize, onView }) => { + const [isOpen, setIsOpen] = React.useState(false); - const [t] = useTranslation(); + const { t } = useTranslation(); - const onSelect = React.useCallback( - (_) => { - setOpen(false); - }, - [setOpen], + const onSelect = React.useCallback((_) => setIsOpen(false), [setIsOpen]); + + const onToggle = React.useCallback(() => setIsOpen((isOpen) => !isOpen), [setIsOpen]); + + const toggle = React.useCallback( + (toggleRef: React.Ref) => ( + + + + ), + [onToggle, isOpen], ); return ( - <> - document.getElementById('dashboard-grid') || portalRoot} - position={'right'} - isOpen={isOpen} - toggle={} - onSelect={onSelect} - dropdownItems={[ - - {t('VIEW', { ns: 'common' })} - , - - {t('REMOVE', { ns: 'common' })} - , - - {t('DashboardCardActionMenu.RESET_SIZE')} - , - ]} - /> - + document.getElementById('dashboard-grid') || portalRoot, + position: 'right', + }} + isOpen={isOpen} + onSelect={onSelect} + toggle={toggle} + onOpenChange={(isOpen) => { + setIsOpen(isOpen); + }} + > + + + {t('VIEW', { ns: 'common' })} + + , + + {t('REMOVE', { ns: 'common' })} + + , + + {t('DashboardCardActionMenu.RESET_SIZE')} + + + ); }; diff --git a/src/app/Dashboard/DashboardLayoutCreateModal.tsx b/src/app/Dashboard/DashboardLayoutCreateModal.tsx index 5d3240f46..60c516191 100644 --- a/src/app/Dashboard/DashboardLayoutCreateModal.tsx +++ b/src/app/Dashboard/DashboardLayoutCreateModal.tsx @@ -26,7 +26,10 @@ import { Button, Form, FormGroup, + FormHelperText, FormSection, + HelperText, + HelperTextItem, Modal, TextInput, Title, @@ -66,7 +69,7 @@ export const DashboardLayoutCreateModal: React.FC { + (_, value: string) => { setName(value); if (value.length === 0) { setNameValidated(ValidatedOptions.error); @@ -90,7 +93,7 @@ export const DashboardLayoutCreateModal: React.FC { + (ev?: React.MouseEvent | KeyboardEvent) => { ev && ev.stopPropagation(); setName(props.oldName || ''); setNameValidated(ValidatedOptions.default); @@ -142,20 +145,22 @@ export const DashboardLayoutCreateModal: React.FC {isCreateModal && ( - +
)} - + + + + + {nameValidated === ValidatedOptions.error + ? errorMessage + : t('DashboardLayoutCreateModal.NAME.HELPER_TEXT')} + + + dashboardConfigs.layouts[dashboardConfigs.current], [dashboardConfigs]); const handleClose = React.useCallback( - (ev?: React.MouseEvent) => { + (ev?: KeyboardEvent | React.MouseEvent) => { ev && ev.stopPropagation(); setName(''); setDescription(''); @@ -93,7 +105,7 @@ export const DashboardLayoutSetAsTemplateModal: React.FC { + (_, value: string) => { setName(value); if (value.length === 0) { setNameValidated(ValidatedOptions.error); @@ -115,7 +127,7 @@ export const DashboardLayoutSetAsTemplateModal: React.FC { + (_, value: string) => { setDescription(value); if (value.length === 0) { setDescriptionValidated(ValidatedOptions.default); @@ -153,14 +165,16 @@ export const DashboardLayoutSetAsTemplateModal: React.FC e.preventDefault()}> - + + + + + {nameValidated === ValidatedOptions.error + ? nameErrorMessage + : t('DashboardLayoutSetAsTemplateModal.FORM_GROUP.NAME.HELPER_TEXT')} + + + - + + + + + {descriptionValidated === ValidatedOptions.error + ? descriptionErrorMessage + : t('DashboardLayoutSetAsTemplateModal.FORM_GROUP.DESCRIPTION.HELPER_TEXT')} + + +