diff --git a/locales/en/common.json b/locales/en/common.json index 620334708..8c2e8c8c8 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -80,7 +80,6 @@ "TEST": "Test", "TIME": "Time", "UNKNOWN_ERROR": "Unknown error", - "UPLOAD": "Upload", "USER_SUBMITTED": "User-submitted", "VIEW": "View", "VIEW_MORE": "View more", diff --git a/locales/en/public.json b/locales/en/public.json index afb7a1272..f79fa5b9b 100644 --- a/locales/en/public.json +++ b/locales/en/public.json @@ -54,7 +54,7 @@ } }, "USER_MENU": { - "LANGUAGE_PREFERENCE": " Language preference", + "LANGUAGE_PREFERENCE": "Language preference", "LOGOUT": "Log out" } }, @@ -77,6 +77,10 @@ "TOOLTIP": "Report data is stale. Click the Create Recording button and choose an option to start an active Recording to source automated reports from." }, "TOOLBAR": { + "ARIA_LABELS": { + "GRID_VIEW": "grid-view", + "LIST_VIEW": "list-view" + }, "CHECKBOX": { "SHOW_NA": { "LABEL": "Show N/A scores" @@ -88,15 +92,14 @@ "LABEL": "Automated analysis toolbar", "REFRESH": { "LABEL": "Refresh automated analysis" - }, - "SWITCH": { - "LIST_VIEW": { - "LABEL": "List view" - } } }, - "WARNING_RESULTS_one": "{{count}} Warning Result", - "WARNING_RESULTS_other": "{{count}} Warning Results" + "TOOLTIP": { + "CLEAR_ANALYSIS": "Clear analysis", + "REFRESH_ANALYSIS": "Refresh analysis" + }, + "WARNING_RESULTS_one": "{{count}} warning result", + "WARNING_RESULTS_other": "{{count}} warning results" }, "AutomatedAnalysisConfigDrawer": { "INPUT_GROUP": { @@ -139,6 +142,7 @@ "POPOUT": { "LABEL": "Pop out {{chartKind}} chart" }, + "REFRESH_TOOLTIP": "Refresh chart data", "SYNC": { "LABEL": "Synchronize {{chartKind}} chart" } @@ -235,7 +239,7 @@ }, "Dashboard": { "ADD_CARD_HELPER_TEXT": "Choose a card type to add to your Dashboard. Some cards require additional configuration.", - "CARD_CATALOG_DESCRIPTION": "Cards added to this Dashboard Layout present information at a glance about the selected target. The layout is preserved for all targets viewed on this client.", + "CARD_CATALOG_DESCRIPTION": "Cards added to this Dashboard Layout present information at a glance about the selected target. The layout is preserved locally for all targets viewed only on this client.", "CARD_CATALOG_TITLE": "Dashboard Card catalog", "INVALID_CARD_CONFIGURATIONS": "Invalid card configurations", "PAGE_TITLE": "Dashboard" @@ -349,10 +353,11 @@ "ITEMS_other": "{{count}} items" }, "LayoutTemplatePicker": { + "CARD_COUNT": "Card count", + "SEARCH_PLACEHOLDER": "Find by name...", "SORT_BY": { "CARD_COUNT": "Sort by: Card count", - "NAME": "Sort by: Name", - "PLACEHOLDER": "Sort by..." + "NAME": "Sort by: Name" } }, "LayoutTemplateUploadModal": { diff --git a/src/app/AppLayout/AppLayout.tsx b/src/app/AppLayout/AppLayout.tsx index f163bec62..3ff314067 100644 --- a/src/app/AppLayout/AppLayout.tsx +++ b/src/app/AppLayout/AppLayout.tsx @@ -82,6 +82,7 @@ import * as React from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { Link, matchPath, NavLink, useLocation, useNavigate } from 'react-router-dom'; import { map } from 'rxjs/operators'; +import { LogoutIcon } from './LogoutIcon'; import { ThemeToggle } from './ThemeToggle'; export interface AppLayoutProps { @@ -283,11 +284,11 @@ export const AppLayout: React.FC = ({ children }) => { const userInfoItems = React.useMemo( () => [ - - }} i18nKey="AppLayout.USER_MENU.LANGUAGE_PREFERENCE" /> + }> + , - + }> {t('AppLayout.USER_MENU.LOGOUT')} , ], diff --git a/src/app/AppLayout/LogoutIcon.tsx b/src/app/AppLayout/LogoutIcon.tsx new file mode 100644 index 000000000..9792cc62d --- /dev/null +++ b/src/app/AppLayout/LogoutIcon.tsx @@ -0,0 +1,31 @@ +/* + * Copyright The Cryostat Authors. + * + * 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 * as React from 'react'; + +export const LogoutIcon: React.FC> = ({ style }) => { + return ( + + + + ); +}; diff --git a/src/app/Dashboard/AddCard.tsx b/src/app/Dashboard/AddCard.tsx index cd186d39d..0c45fafd6 100644 --- a/src/app/Dashboard/AddCard.tsx +++ b/src/app/Dashboard/AddCard.tsx @@ -21,13 +21,12 @@ import { fakeChartContext, fakeServices } from '@app/utils/fakeData'; import { useFeatureLevel } from '@app/utils/hooks/useFeatureLevel'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; import { portalRoot } from '@app/utils/utils'; +import { CatalogTile, CatalogTileBadge } from '@patternfly/react-catalog-view-extension'; import { Bullseye, Button, Card, CardBody, - CardHeader, - CardTitle, Drawer, DrawerActions, DrawerCloseButton, @@ -48,8 +47,6 @@ import { GridItem, Label, LabelGroup, - Level, - LevelItem, Modal, NumberInput, Stack, @@ -322,48 +319,35 @@ export const CardGallery: React.FC = ({ selection, onSelect }) return availableCards.map((card) => { const { icon, labels, title, description } = card; return ( - { + if (selection === t(title)) { + setToViewCard(availableCards.find((card) => t(card.title) === selection)); + } else { + onSelect(_event, t(title)); + } + }} + badges={ + labels && [ + + + {labels.map(({ content, icon, color }) => ( + + ))} + + , + ] + } > - { - if (selection === t(title)) { - setToViewCard(availableCards.find((card) => t(card.title) === selection)); - } else { - onSelect(event, t(title)); - } - }, - selectableActionId: title, - }} - > - - {icon ? {icon} : null} - - {t(title)} - - - {labels ? ( - - {labels.map(({ content, icon, color }) => ( - - ))} - - ) : null} - - - - {t(description)} - + {t(description)} + ); }); }, [t, availableCards, selection, onSelect]); @@ -392,19 +376,19 @@ export const CardGallery: React.FC = ({ selection, onSelect }) {t(title)} + + {labels && labels.length ? ( + + {labels.map(({ content, icon, color }) => ( + + ))} + + ) : null} + - - {labels && labels.length ? ( - - {labels.map(({ content, icon, color }) => ( - - ))} - - ) : null} - {getFullDescription(t(title), t)} {preview ? ( @@ -436,7 +420,7 @@ export const CardGallery: React.FC = ({ selection, onSelect }) - + {items.map((item) => ( {item} @@ -544,12 +528,12 @@ const PropsConfigForm: React.FC = ({ onChange, ...props }) } return ( + {input} {t(ctrl.description)} - {input} ); }, @@ -639,7 +623,10 @@ const SelectControl: React.FC = ({ handleChange, control, se enableFlip: true, appendTo: portalRoot, }} - shouldFocusToggleOnSelect + isScrollable + maxMenuHeight={'30vh'} + onOpenChange={setSelectOpen} + onOpenChangeKeys={['Escape']} > {errored diff --git a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx index 99807df76..861e7c3bf 100644 --- a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx +++ b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisCard.tsx @@ -65,11 +65,8 @@ import { GridItem, Label, LabelGroup, - Level, - LevelItem, Stack, StackItem, - Switch, Text, TextContent, TextVariants, @@ -81,6 +78,10 @@ import { EmptyStateActions, EmptyStateHeader, EmptyStateFooter, + Flex, + FlexItem, + ToggleGroup, + ToggleGroupItem, } from '@patternfly/react-core'; import { CheckCircleIcon, @@ -673,45 +674,61 @@ export const AutomatedAnalysisCard: DashboardCardFC filters={targetAutomatedAnalysisFilters} updateFilters={updateFilters} /> - + + setShowNAScores(checked)} + id="show-na-scores" + name="show-na-scores" + style={{ alignSelf: 'center' }} + /> + + + - - + + ); diff --git a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisFilters.tsx b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisFilters.tsx index 63b907fcc..1a182c43b 100644 --- a/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisFilters.tsx +++ b/src/app/Dashboard/AutomatedAnalysis/AutomatedAnalysisFilters.tsx @@ -29,7 +29,7 @@ import { MenuToggle, MenuToggleElement, } from '@patternfly/react-core'; -import { EllipsisVIcon, FilterIcon } from '@patternfly/react-icons'; +import { FilterIcon } from '@patternfly/react-icons'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; @@ -128,13 +128,13 @@ export const AutomatedAnalysisFilters: React.FC = return ( ) => ( - onCategoryToggle()}> - + }> {getCategoryDisplay(currentCategory)} - )} isOpen={isCategoryDropdownOpen} + onOpenChange={setIsCategoryDropdownOpen} + onOpenChangeKeys={['Escape']} popperProps={{ position: 'left', }} diff --git a/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisNameFilter.tsx b/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisNameFilter.tsx index 693be9c0e..54bb55e83 100644 --- a/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisNameFilter.tsx +++ b/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisNameFilter.tsx @@ -29,6 +29,7 @@ import { TextInputGroupUtilities, } from '@patternfly/react-core'; import { TimesIcon } from '@patternfly/react-icons'; +import _ from 'lodash'; import * as React from 'react'; export interface AutomatedAnalysisNameFilterProps { @@ -70,7 +71,8 @@ export const AutomatedAnalysisNameFilter: React.FC { - return !filterValue ? nameOptions : nameOptions.filter((n) => n.includes(filterValue.toLowerCase())); + const reg = new RegExp(_.escapeRegExp(filterValue), 'i'); + return !filterValue ? nameOptions : nameOptions.filter((n) => reg.test(n)); }, [filterValue, nameOptions]); const selectOptionProps: SelectOptionProps[] = React.useMemo(() => { @@ -124,6 +126,11 @@ export const AutomatedAnalysisNameFilter: React.FC document.getElementById('dashboard-grid') || portalRoot, }} + onOpenChange={setIsExpanded} + onOpenChangeKeys={['Escape']} + shouldFocusFirstItemOnOpen={false} + isScrollable + maxMenuHeight={'30vh'} > {selectOptionProps.map(({ value, children }, index) => ( diff --git a/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisTopicFilter.tsx b/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisTopicFilter.tsx index 1256d80cd..ce06251b2 100644 --- a/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisTopicFilter.tsx +++ b/src/app/Dashboard/AutomatedAnalysis/Filters/AutomatedAnalysisTopicFilter.tsx @@ -29,6 +29,7 @@ import { TextInputGroupUtilities, } from '@patternfly/react-core'; import { TimesIcon } from '@patternfly/react-icons'; +import _ from 'lodash'; import * as React from 'react'; export interface AutomatedAnalysisTopicFilterProps { @@ -60,7 +61,8 @@ export const AutomatedAnalysisTopicFilter: React.FC { - return !filterValue ? topicOptions : topicOptions.filter((topic) => topic.includes(filterValue.toLowerCase())); + const reg = new RegExp(_.escapeRegExp(filterValue), 'i'); + return !filterValue ? topicOptions : topicOptions.filter((topic) => reg.test(topic)); }, [filterValue, topicOptions]); const selectOptionProps: SelectOptionProps[] = React.useMemo(() => { @@ -113,6 +115,11 @@ export const AutomatedAnalysisTopicFilter: React.FC document.getElementById('dashboard-grid') || portalRoot, }} + onOpenChange={setIsExpanded} + onOpenChangeKeys={['Escape']} + shouldFocusFirstItemOnOpen={false} + isScrollable + maxMenuHeight={'30vh'} > {selectOptionProps.map(({ value, children }, index) => ( diff --git a/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx b/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx index 739fea348..085f5330b 100644 --- a/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx +++ b/src/app/Dashboard/Charts/jfr/JFRMetricsChartCard.tsx @@ -56,30 +56,34 @@ export interface JFRMetricsChartCardProps extends DashboardCardTypeProps { // TODO these need to be localized export enum JFRMetricsChartKind { - 'Core Count' = 1, - 'Thread Count' = 2, - 'CPU Load' = 3, - 'Heap Usage' = 4, - 'Memory Usage' = 5, - 'Total Memory' = 6, - 'Recording Start Time' = 7, - 'Recording Duration' = 8, - 'Classloading Statistics' = 9, - 'Metaspace Summary' = 10, + 'Recording Duration' = 2, + 'Recording Start Time' = 3, + + 'Core Count' = 5, + 'Thread Count' = 6, + 'Total Memory' = 7, + 'CPU Load' = 8, + 'Memory Usage' = 9, + 'Network Utilization' = 11, - 'Metaspace GC Threshold' = 12, - 'Thread Statistics' = 13, - 'Exception Statistics' = 14, - 'Thread Context Switch Rate' = 15, - 'Compiler Statistics' = 16, - 'Safepoint Duration' = 18, - 'File I/O' = 19, - 'Compiler Total Time' = 20, + 'File I/O' = 13, + + 'Thread Statistics' = 15, + 'Thread Context Switch Rate' = 16, + + 'Heap Usage' = 18, + 'Object Allocation Sample' = 19, + 'Safepoint Duration' = 20, + 'Metaspace Summary' = 21, + 'Metaspace GC Threshold' = 22, 'Compiler Peak Time' = 24, + 'Compiler Total Time' = 25, + 'Compiler Statistics' = 26, - 'Object Allocation Sample' = 38, + 'Classloading Statistics' = 28, + 'Exception Statistics' = 29, } export function kindToId(kind: string): number { @@ -338,7 +342,7 @@ export const JFRMetricsChartCardDescriptor: DashboardCardDescriptor = { labels: [ { content: 'Beta', - color: 'green', + color: 'cyan', }, { content: 'Metrics', diff --git a/src/app/Dashboard/Charts/mbean/MBeanMetricsChartCard.tsx b/src/app/Dashboard/Charts/mbean/MBeanMetricsChartCard.tsx index 6c8f722e6..98ba75456 100644 --- a/src/app/Dashboard/Charts/mbean/MBeanMetricsChartCard.tsx +++ b/src/app/Dashboard/Charts/mbean/MBeanMetricsChartCard.tsx @@ -40,7 +40,7 @@ import { ChartLine, ChartVoronoiContainer, } from '@patternfly/react-charts'; -import { getResizeObserver, Button, CardBody, CardHeader, CardTitle } from '@patternfly/react-core'; +import { getResizeObserver, Button, CardBody, CardHeader, CardTitle, Tooltip } from '@patternfly/react-core'; import { MonitoringIcon, SyncAltIcon } from '@patternfly/react-icons'; import _ from 'lodash'; import * as React from 'react'; @@ -124,8 +124,8 @@ const SimpleChart: React.FC<{ style={{ fill: cryostatTheme === ThemeSetting.DARK - ? 'var(--pf-global--palette--black-200)' - : 'var(--pf-chart-global--label--Fill, #151515)', + ? 'var(--pf-v5-global--palette--black-200)' + : 'var(--pf-v5-chart-global--label--Fill, #151515)', }} /> } @@ -151,8 +151,8 @@ const SimpleChart: React.FC<{ style={{ fill: cryostatTheme === ThemeSetting.DARK - ? 'var(--pf-global--palette--black-200)' - : 'var(--pf-chart-global--label--Fill, #151515)', + ? 'var(--pf-v5-global--palette--black-200)' + : 'var(--pf-v5-chart-global--label--Fill, #151515)', }} /> } @@ -297,8 +297,8 @@ const chartKinds: MBeanMetricsChartKind[] = [ style={{ fill: cryostatTheme === ThemeSetting.DARK - ? 'var(--pf-global--palette--black-200)' - : 'var(--pf-chart-donut--label--title--Fill, #151515)', + ? 'var(--pf-v5-global--palette--black-200)' + : 'var(--pf-v5-chart-donut--label--title--Fill, #151515)', fontSize: '24px', }} /> @@ -375,7 +375,7 @@ export const MBeanMetricsChartCard: DashboardCardFC const isError = React.useMemo(() => errorMessage != '', [errorMessage]); const resizeObserver = React.useRef((): void => undefined); - const [cardWidth, setCardWidth] = React.useState(0); + const [cardWidth, setCardWidth] = React.useState(1); // Use non-zero as 0 means Infinity (invalid) /* eslint-disable @typescript-eslint/no-explicit-any */ const containerRef: React.Ref = React.createRef(); @@ -402,7 +402,7 @@ export const MBeanMetricsChartCard: DashboardCardFC }, [containerRef, setCardWidth]); React.useEffect(() => { - resizeObserver.current = getResizeObserver(containerRef.current, handleResize); + resizeObserver.current = getResizeObserver(containerRef.current, handleResize, true); handleResize(); return resizeObserver.current; }, [resizeObserver, containerRef, handleResize]); @@ -459,14 +459,15 @@ export const MBeanMetricsChartCard: DashboardCardFC const refreshButton = React.useMemo( () => ( - ), [t, handleUploadButton], @@ -234,9 +246,6 @@ export const LayoutTemplatePicker: React.FC = ({ onTe const onFilterSelect = React.useCallback( (_ev: React.MouseEvent, selected: LayoutTemplateFilter) => { setSelectedFilters((prev) => { - if (selected) { - return []; - } if (prev.includes(selected)) { return prev.filter((item) => item !== selected); } @@ -429,9 +438,9 @@ export const LayoutTemplatePicker: React.FC = ({ onTe }, [t, onDrawerCloseClick, selectedTemplate]); const sortedFilteredFeatureLeveledTemplateLayoutGroup = React.useCallback( - (title: LayoutTemplateFilter, templates: LayoutTemplate[]) => { - const featuredLevelled = templates.filter((t) => smallestFeatureLevel(t.cards) >= activeLevel); - const sortedSearchFilteredTemplates = searchFilteredTemplates(featuredLevelled).sort((a, b) => { + (title: LayoutTemplateFilter, templates: LayoutTemplate[], divider: boolean = true) => { + const featuredLevelledTemplates = templates.filter((t) => smallestFeatureLevel(t.cards) >= activeLevel); + const sortedSearchFilteredTemplates = searchFilteredTemplates(featuredLevelledTemplates).sort((a, b) => { switch (selectedSort) { case 'Name': if (sortDirection === 'asc') { @@ -465,13 +474,13 @@ export const LayoutTemplatePicker: React.FC = ({ onTe onTemplateDelete={onInnerTemplateDelete} /> - - - + {divider && ( + + + + )} - ) : ( - <> - ); + ) : null; }, [ activeLevel, @@ -510,120 +519,142 @@ export const LayoutTemplatePicker: React.FC = ({ onTe - - - - - } breakpoint={'md'}> - - - t.name)} onChange={onSearchChange} /> - - - ) => ( + {selectedFilters.length} : null} > - - - {t('SUGGESTED', { ns: 'common' })} - - - Cryostat - - - {t('USER_SUBMITTED', { ns: 'common' })} - - - - - - - - - - - - - - - - {uploadButton} - - - - - {allSearchableTemplateNames.length !== 0 ? ( - <> - {sortedFilteredFeatureLeveledTemplateLayoutGroup(t('SUGGESTED', { ns: 'common' }), [ - BlankLayout, - ...RecentTemplates, - ])} - {sortedFilteredFeatureLeveledTemplateLayoutGroup('Cryostat', CryostatLayoutTemplates)} - {sortedFilteredFeatureLeveledTemplateLayoutGroup( - t('USER_SUBMITTED', { ns: 'common' }), - userSubmittedTemplates, + Template Type + + )} + aria-label="Select template category" + onSelect={onFilterSelect} + selected={selectedFilters} + isOpen={isFilterSelectOpen} + onOpenChangeKeys={['Escape']} + onOpenChange={setIsFilterSelectOpen} + > + + + {t('SUGGESTED', { ns: 'common' })} + + + Cryostat + + + {t('USER_SUBMITTED', { ns: 'common' })} + + + + + + + + + + + + + + + + + + {uploadButton} + + + + + + {allSearchableTemplateNames.length > 0 ? ( + <> + {sortedFilteredFeatureLeveledTemplateLayoutGroup(t('SUGGESTED', { ns: 'common' }), [ + BlankLayout, + ...RecentTemplates, + ])} + {sortedFilteredFeatureLeveledTemplateLayoutGroup( + 'Cryostat', + CryostatLayoutTemplates, + userSubmittedTemplates.length > 0, + )} + {sortedFilteredFeatureLeveledTemplateLayoutGroup( + t('USER_SUBMITTED', { ns: 'common' }), + userSubmittedTemplates, + false, + )} + + ) : ( + + + } + headingLevel="h4" + /> + Upload a template and try again. + + + )} + + {deleteWarningModal} diff --git a/src/app/Dashboard/SearchAutocomplete.tsx b/src/app/Dashboard/SearchAutocomplete.tsx index 897a1dea1..1a6d6aa78 100644 --- a/src/app/Dashboard/SearchAutocomplete.tsx +++ b/src/app/Dashboard/SearchAutocomplete.tsx @@ -20,10 +20,11 @@ import * as React from 'react'; export interface SearchAutocompleteProps { values: string[]; onChange: (value: string) => void; + placeholder?: string; } -export const SearchAutocomplete: React.FC = ({ onChange, ...props }) => { - const [value, setValue] = React.useState(''); +export const SearchAutocomplete: React.FC = ({ onChange, placeholder, ...props }) => { + const [searchTerm, setSearchTerm] = React.useState(''); const [hint, setHint] = React.useState(''); const [autocompleteOptions, setAutocompleteOptions] = React.useState([]); @@ -33,9 +34,9 @@ export const SearchAutocomplete: React.FC = ({ onChange const autocompleteRef = React.useRef(null); const onClear = React.useCallback(() => { - setValue(''); + setSearchTerm(''); onChange(''); - }, [setValue, onChange]); + }, [setSearchTerm, onChange]); const onSearchChange = React.useCallback( (newValue) => { @@ -61,10 +62,10 @@ export const SearchAutocomplete: React.FC = ({ onChange setAutocompleteOptions([]); setHint(''); } - setValue(newValue); + setSearchTerm(newValue); onChange(newValue); }, - [setValue, setHint, setIsAutocompleteOpen, setAutocompleteOptions, onChange, props.values], + [setSearchTerm, setHint, setIsAutocompleteOpen, setAutocompleteOptions, onChange, props.values], ); // Whenever an autocomplete option is selected, set the search input value, close the menu, and put the browser @@ -72,12 +73,12 @@ export const SearchAutocomplete: React.FC = ({ onChange const onSelect = React.useCallback( (e, itemId) => { e.stopPropagation(); - setValue(itemId); + setSearchTerm(itemId); setHint(''); setIsAutocompleteOpen(false); searchInputRef.current && searchInputRef.current.focus(); }, - [setValue, setHint, setIsAutocompleteOpen], + [setSearchTerm, setHint, setIsAutocompleteOpen], ); const handleMenuKeys = React.useCallback( @@ -85,7 +86,7 @@ export const SearchAutocomplete: React.FC = ({ onChange // If there is a hint while the browser focus is on the search input, tab or right arrow will 'accept' the hint value // and set it as the search input value if (hint && (event.key === 'Tab' || event.key === 'ArrowRight') && searchInputRef.current === event.target) { - setValue(hint); + setSearchTerm(hint); setHint(''); setIsAutocompleteOpen(false); if (event.key === 'ArrowRight') { @@ -150,15 +151,16 @@ export const SearchAutocomplete: React.FC = ({ onChange const searchInput = React.useMemo( () => ( ), - [value, onSearchChange, onClear, hint], + [searchTerm, onSearchChange, onClear, hint, placeholder], ); const autocomplete = React.useMemo( diff --git a/src/app/Dashboard/cryostat-dashboard-templates.tsx b/src/app/Dashboard/cryostat-dashboard-templates.tsx index 562d6473f..6c1d60472 100644 --- a/src/app/Dashboard/cryostat-dashboard-templates.tsx +++ b/src/app/Dashboard/cryostat-dashboard-templates.tsx @@ -15,7 +15,7 @@ */ import { LayoutTemplate, LayoutTemplateVendor, LayoutTemplateVersion } from './types'; -const CURR_VERSION: LayoutTemplateVersion = LayoutTemplateVersion['v2.4']; +const CURR_VERSION: LayoutTemplateVersion = LayoutTemplateVersion['v3.0']; export const BlankLayout: LayoutTemplate = { name: 'Blank', diff --git a/src/app/Dashboard/types.ts b/src/app/Dashboard/types.ts index 659531459..965f354c4 100644 --- a/src/app/Dashboard/types.ts +++ b/src/app/Dashboard/types.ts @@ -121,6 +121,8 @@ export type LayoutTemplateRecord = Pick; export enum LayoutTemplateVersion { 'v2.3' = 'v2.3', 'v2.4' = 'v2.4', + 'v3.0' = 'v3.0', + 'v4.0' = 'v4.0', } export enum LayoutTemplateVendor { diff --git a/src/app/QuickStarts/QuickStartDrawer.tsx b/src/app/QuickStarts/QuickStartDrawer.tsx index e95cd42f0..52cbedbe1 100644 --- a/src/app/QuickStarts/QuickStartDrawer.tsx +++ b/src/app/QuickStarts/QuickStartDrawer.tsx @@ -78,7 +78,7 @@ export const GlobalQuickStartDrawer: React.FC = ({ regex: HIGHLIGHT_REGEXP, replace: (text: string, linkLabel: string, linkType: string, linkId: string): string => { if (!linkLabel || !linkType || !linkId) return text; - return ``; + return ``; }, }, { diff --git a/src/app/app.css b/src/app/app.css index 81e514737..ecb335ab2 100644 --- a/src/app/app.css +++ b/src/app/app.css @@ -114,13 +114,29 @@ body, overflow-y: auto; } +.dashboard-card-picker { + align-items: stretch; + margin-top: 1em; + /* Ensure left border is visible*/ + margin-left: 1px; +} + +.dashboard-card-picker .catalog-tile-pf { + height: 100%; + border-top: 1.5px solid var(--pf-v5-global--palette--black-300); +} + .layout-template-picker { padding: 0 1em 1em 1em; } +.catalog-tile-pf-icon { + /* Catalog icon style is only applied if display is inline-block */ + display: inline-block; +} + .layout-template-picker .catalog-tile-pf { height: 100%; - color: var(--pf-v5-global--Color--100); border-top: 1.5px solid var(--pf-v5-global--palette--black-300); } @@ -135,6 +151,10 @@ body, z-index: 1; } +.layout-template-card__action-toggle { + z-index: 1; +} + #dashboard-layout-menu-content .pf-v5-c-menu__item-action { padding-right: 0.65em; padding-left: 0.65em; diff --git a/src/app/index.tsx b/src/app/index.tsx index 0ec3bb9a7..4c96f6346 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -15,6 +15,8 @@ */ import '@patternfly/react-core/dist/styles/base.css'; +import '@patternfly/react-catalog-view-extension/dist/css/react-catalog-view-extension.css'; +import '@patternfly/quickstarts/dist/quickstarts.min.css'; import '@app/app.css'; import '@app/Topology/styles/base.css'; import '@i18n/config'; diff --git a/stylePaths.js b/stylePaths.js new file mode 100644 index 000000000..b432f3c26 --- /dev/null +++ b/stylePaths.js @@ -0,0 +1,19 @@ +const path = require('path'); +module.exports = { + stylePaths: [ + path.resolve(__dirname, 'src'), + path.resolve(__dirname, 'node_modules/patternfly'), + path.resolve(__dirname, 'node_modules/@patternfly/patternfly'), + path.resolve(__dirname, 'node_modules/@patternfly/react-styles/css'), + path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/base.css'), + path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/esm/@patternfly/patternfly'), + path.resolve(__dirname, 'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css'), + path.resolve(__dirname, 'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css'), + path.resolve(__dirname, 'node_modules/@patternfly/quickstarts/dist/quickstarts.css'), + path.resolve(__dirname, 'node_modules/@patternfly/react-topology/dist/esm/css'), + path.resolve(__dirname, "node_modules/@patternfly/react-topology/node_modules/@patternfly/react-styles/css"), + path.resolve(__dirname, "node_modules/@patternfly/react-catalog-view-extension/dist/css/react-catalog-view-extension.css"), + path.resolve(__dirname, "node_modules/@patternfly/react-catalog-view-extension/node_modules/@patternfly/react-styles/css"), + path.resolve(__dirname, 'node_modules/@patternfly/quickstarts/dist/quickstarts.min.css'), + ] +} diff --git a/webpack.dev.js b/webpack.dev.js index 186b9a18b..5b445e701 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -3,6 +3,7 @@ const { merge } = require('webpack-merge'); const common = require('./webpack.common.js'); const ESLintPlugin = require('eslint-webpack-plugin'); const { EnvironmentPlugin } = require('webpack'); +const { stylePaths } = require('./stylePaths'); const HOST = process.env.HOST || "localhost"; const PORT = process.env.PORT || "9000"; @@ -41,20 +42,7 @@ module.exports = merge(common('development'), { rules: [ { test: /\.css$/, - include: [ - path.resolve(__dirname, 'src'), - path.resolve(__dirname, 'node_modules/patternfly'), - path.resolve(__dirname, 'node_modules/@patternfly/patternfly'), - path.resolve(__dirname, 'node_modules/@patternfly/react-styles/css'), - path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/base.css'), - path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/esm/@patternfly/patternfly'), - path.resolve(__dirname, 'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css'), - path.resolve(__dirname, 'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css'), - path.resolve(__dirname, 'node_modules/@patternfly/quickstarts/dist/quickstarts.css'), - path.resolve(__dirname, 'node_modules/@patternfly/react-topology/dist/esm/css'), - path.resolve(__dirname, "node_modules/@patternfly/react-topology/node_modules/@patternfly/react-styles/css"), - path.resolve(__dirname, "node_modules/@patternfly/react-catalog-view-extension/node_modules/@patternfly/react-styles/css") - ], + include: [...stylePaths], use: ['style-loader', 'css-loader'] } ] diff --git a/webpack.prod.js b/webpack.prod.js index 1d7e92bc8..a8bad530d 100644 --- a/webpack.prod.js +++ b/webpack.prod.js @@ -5,6 +5,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const TerserJSPlugin = require('terser-webpack-plugin'); const { EnvironmentPlugin } = require('webpack'); +const { stylePaths } = require('./stylePaths'); module.exports = merge(common('production'), { mode: 'production', @@ -50,20 +51,7 @@ module.exports = merge(common('production'), { rules: [ { test: /\.css$/, - include: [ - path.resolve(__dirname, 'src'), - path.resolve(__dirname, 'node_modules/patternfly'), - path.resolve(__dirname, 'node_modules/@patternfly/patternfly'), - path.resolve(__dirname, 'node_modules/@patternfly/react-styles/css'), - path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/base.css'), - path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/esm/@patternfly/patternfly'), - path.resolve(__dirname, 'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css'), - path.resolve(__dirname, 'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css'), - path.resolve(__dirname, 'node_modules/@patternfly/quickstarts/dist/quickstarts.css'), - path.resolve(__dirname, 'node_modules/@patternfly/react-topology/dist/esm/css'), - path.resolve(__dirname, "node_modules/@patternfly/react-topology/node_modules/@patternfly/react-styles/css"), - path.resolve(__dirname, "node_modules/@patternfly/react-catalog-view-extension/node_modules/@patternfly/react-styles/css") - ], + include: [...stylePaths], use: [MiniCssExtractPlugin.loader, 'css-loader'] } ]