Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(pf5): allow menu footer to be sticky and use danger style for delete/clear buttons #1346

Merged
merged 5 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/Dashboard/AddCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ const SelectControl: React.FC<SelectControlProps> = ({ handleChange, control, se
appendTo: portalRoot,
}}
isScrollable
maxMenuHeight={'30vh'}
maxMenuHeight="40vh"
onOpenChange={setSelectOpen}
onOpenChangeKeys={['Escape']}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export const AutomatedAnalysisNameFilter: React.FC<AutomatedAnalysisNameFilterPr
onOpenChangeKeys={['Escape']}
shouldFocusFirstItemOnOpen={false}
isScrollable
maxMenuHeight={'30vh'}
maxMenuHeight="40vh"
>
<SelectList id="typeahead-filter-select">
{selectOptionProps.map(({ value, children }, index) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export const AutomatedAnalysisTopicFilter: React.FC<AutomatedAnalysisTopicFilter
onOpenChangeKeys={['Escape']}
shouldFocusFirstItemOnOpen={false}
isScrollable
maxMenuHeight={'30vh'}
maxMenuHeight="40vh"
>
<SelectList id="typeahead-topic-filter">
{selectOptionProps.map(({ value, children }, index) => (
Expand Down
10 changes: 6 additions & 4 deletions src/app/Dashboard/DashboardCardActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/
import { portalRoot } from '@app/utils/utils';
import { Dropdown, DropdownItem, DropdownList, MenuToggle, MenuToggleElement } from '@patternfly/react-core';
import { Divider, 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';
Expand Down Expand Up @@ -66,12 +66,14 @@ export const DashboardCardActionMenu: React.FC<DashboardCardActionProps> = ({ on
<DropdownItem key="View" onClick={onView}>
{t('VIEW', { ns: 'common' })}
</DropdownItem>
<DropdownItem key="Remove" onClick={onRemove}>
{t('REMOVE', { ns: 'common' })}
</DropdownItem>

<DropdownItem key="Reset Size" onClick={onResetSize}>
{t('DashboardCardActionMenu.RESET_SIZE')}
</DropdownItem>
<Divider />
<DropdownItem key="Remove" onClick={onRemove} isDanger>
{t('REMOVE', { ns: 'common' })}
</DropdownItem>
</DropdownList>
</Dropdown>
);
Expand Down
5 changes: 3 additions & 2 deletions src/app/Dashboard/DashboardLayoutToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,8 @@ export const DashboardLayoutToolbar: React.FC<DashboardLayoutToolbarProps> = (_p
<DropdownItem key="download" value={'download'}>
{t('DashboardLayoutToolbar.DOWNLOAD_AS_TEMPLATE')}
</DropdownItem>
<DropdownItem key="clearAll" value={'clearAll'} isDisabled={currLayout.cards.length < 1}>
<Divider />
<DropdownItem key="clearAll" value={'clearAll'} isDisabled={currLayout.cards.length < 1} isDanger>
{t('DashboardLayoutToolbar.CLEAR_LAYOUT')}
</DropdownItem>
</>
Expand Down Expand Up @@ -499,7 +500,7 @@ export const DashboardLayoutToolbar: React.FC<DashboardLayoutToolbarProps> = (_p
)}
>
<Menu isScrollable onSelect={onLayoutSelect} onActionClick={onActionClick}>
<MenuContent maxMenuHeight="21.5em" id="dashboard-layout-menu-content">
<MenuContent maxMenuHeight="40vh" id="dashboard-layout-menu-content">
{menuGroups(t('DashboardLayoutToolbar.MENU.FAVORITES'), true)}
<Divider />
{menuGroups(t('DashboardLayoutToolbar.MENU.OTHERS'), false)}
Expand Down
4 changes: 3 additions & 1 deletion src/app/Dashboard/LayoutTemplateGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
DropdownList,
MenuToggle,
MenuToggleElement,
Divider,
} from '@patternfly/react-core';
import { EllipsisVIcon } from '@patternfly/react-icons';
import * as React from 'react';
Expand Down Expand Up @@ -175,7 +176,8 @@ export const KebabCatalogTileBadge: React.FC<KebabCatalogTileBadgeProps> = ({ te
<DropdownItem key={'download'} onClick={handleTemplateDownload}>
{t('DOWNLOAD', { ns: 'common' })}
</DropdownItem>,
<DropdownItem key={'delete'} onClick={handleTemplateDelete}>
<Divider key="divider" />,
<DropdownItem key={'delete'} onClick={handleTemplateDelete} isDanger>
{t('DELETE', { ns: 'common' })}
</DropdownItem>,
];
Expand Down
2 changes: 1 addition & 1 deletion src/app/DateTimePicker/TimezonePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export const TimezonePicker: React.FC<TimezonePickerProps> = ({
onOpenChange={(isOpen) => setIsTimezoneOpen(isOpen)}
onOpenChangeKeys={['Escape']}
isScrollable
menuHeight="20vh"
maxMenuHeight="40vh"
>
<MenuSearch>
<MenuSearchInput>
Expand Down
1 change: 1 addition & 0 deletions src/app/Events/EventTemplates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ export const EventTemplates: React.FC<EventTemplatesProps> = () => {
{
title: 'Delete',
onClick: () => handleDeleteButton(t),
isDanger: true,
},
]);
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/Recordings/Filters/DatetimeFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export interface DateTimeRange {
to?: Date;
}

export const filterRecordingByDatetime = (recordings?: ActiveRecording[], filters?: DateTimeRange[]) => {
export const filterRecordingByDatetime = (recordings: ActiveRecording[], filters?: DateTimeRange[]) => {
if (!recordings?.length || !filters?.length) {
return recordings;
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/Recordings/Filters/DurationFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { DURATION_INPUT_MAXLENGTH } from './const';

export const CONTINUOUS_INDICATOR = 'continuous';

export const filterRecordingByDuration = (recordings?: ActiveRecording[], filters?: DurationRange[]) => {
export const filterRecordingByDuration = (recordings: ActiveRecording[], filters?: DurationRange[]) => {
if (!recordings?.length || !filters?.length) {
return recordings;
}
Expand Down
5 changes: 3 additions & 2 deletions src/app/Rules/Rules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { MatchExpressionDisplay } from '@app/Shared/Components/MatchExpression/M
import { Rule, NotificationCategory } from '@app/Shared/Services/api.types';
import { ServiceContext } from '@app/Shared/Services/Services';
import { useSubscriptions } from '@app/utils/hooks/useSubscriptions';
import { TableColumn, formatBytes, formatDuration, sortResources } from '@app/utils/utils';
import { TableColumn, formatBytes, formatDuration, sortResources, portalRoot } from '@app/utils/utils';
import {
Button,
Card,
Expand Down Expand Up @@ -301,6 +301,7 @@ export const RulesTable: React.FC<RulesTableProps> = () => {
{
title: t('DELETE', { ns: 'common' }),
onClick: () => handleDeleteButton(rule),
isDanger: true,
},
];
},
Expand Down Expand Up @@ -366,7 +367,7 @@ export const RulesTable: React.FC<RulesTableProps> = () => {
<ActionsColumn
items={actionResolver(r)}
popperProps={{
appendTo: () => document.getElementById('automated-rule-toolbar') || document.body,
appendTo: portalRoot,
position: 'right',
}}
/>
Expand Down
26 changes: 12 additions & 14 deletions src/app/Settings/Config/DatetimeControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,24 +126,22 @@ const Component = () => {
}}
selected={datetimeFormat.dateLocale}
onSelect={handleDateLocaleSelect}
menuHeight="20vh"
maxMenuHeight="40vh"
isScrollable
onOpenChange={setDateLocaleOpen}
onOpenChangeKeys={['Escape']}
>
<SelectList>
<MenuSearch>
<MenuSearchInput>
<SearchInput
placeholder={t('SETTINGS.DATETIME_CONTROL.SEARCH_PLACEHOLDER')}
value={searchTerm}
onChange={onInputChange}
/>
</MenuSearchInput>
</MenuSearch>
<Divider />
{dateLocaleOptions}
</SelectList>
<MenuSearch>
<MenuSearchInput>
<SearchInput
placeholder={t('SETTINGS.DATETIME_CONTROL.SEARCH_PLACEHOLDER')}
value={searchTerm}
onChange={onInputChange}
/>
</MenuSearchInput>
</MenuSearch>
<Divider />
<SelectList>{dateLocaleOptions}</SelectList>
</Select>
</FormGroup>
</StackItem>
Expand Down
32 changes: 32 additions & 0 deletions src/app/Shared/Components/ScrollableMenuContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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 { Panel, PanelMain, PanelMainBody } from '@patternfly/react-core';
import * as React from 'react';

export interface ScrollableMenuContentProps {
maxHeight?: string;
}

// Use case: Menu footer or search bar needs to be sticky.
export const ScrollableMenuContent: React.FC<ScrollableMenuContentProps> = ({ children, maxHeight }) => {
return (
<Panel isScrollable>
<PanelMain maxHeight={maxHeight}>
<PanelMainBody style={{ padding: 0 }}>{children}</PanelMainBody>
</PanelMain>
</Panel>
);
};
46 changes: 24 additions & 22 deletions src/app/TargetView/TargetContextSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/
import { LinearDotSpinner } from '@app/Shared/Components/LinearDotSpinner';
import { ScrollableMenuContent } from '@app/Shared/Components/ScrollableMenuContent';
import { Target } from '@app/Shared/Services/api.types';
import { isEqualTarget, getTargetRepresentation } from '@app/Shared/Services/api.utils';
import { ServiceContext } from '@app/Shared/Services/Services';
Expand All @@ -33,8 +34,8 @@ import {
MenuToggle,
MenuSearch,
MenuSearchInput,
Split,
SplitItem,
ActionList,
ActionListItem,
} from '@patternfly/react-core';
import _ from 'lodash';
import * as React from 'react';
Expand Down Expand Up @@ -206,18 +207,18 @@ export const TargetContextSelector: React.FC<TargetContextSelectorProps> = ({ cl

const selectFooter = React.useMemo(
() => (
<Split hasGutter>
<SplitItem>
<ActionList>
<ActionListItem>
<Button variant="secondary" component={(props) => <Link {...props} to={'/topology/create-custom-target'} />}>
{t('TargetContextSelector.CREATE_TARGET')}
</Button>
</SplitItem>
<SplitItem>
<Button variant="tertiary" onClick={onClearSelection}>
</ActionListItem>
<ActionListItem>
<Button variant="link" onClick={onClearSelection}>
{t('TargetContextSelector.CLEAR_SELECTION')}
</Button>
</SplitItem>
</Split>
</ActionListItem>
</ActionList>
),
[t, onClearSelection],
);
Expand All @@ -230,10 +231,9 @@ export const TargetContextSelector: React.FC<TargetContextSelectorProps> = ({ cl
) : (
<Dropdown
className={className}
isScrollable
placeholder={t('TargetContextSelector.TOGGLE_PLACEHOLDER')}
isOpen={isTargetOpen}
onOpenChange={(isOpen) => setIsTargetOpen(isOpen)}
onOpenChange={setIsTargetOpen}
onOpenChangeKeys={['Escape']}
onSelect={onSelect}
onActionClick={onFavoriteClick}
Expand All @@ -256,17 +256,19 @@ export const TargetContextSelector: React.FC<TargetContextSelectorProps> = ({ cl
appendTo: portalRoot,
}}
>
<MenuSearch>
<MenuSearchInput>
<SearchInput
placeholder={t('TargetContextSelector.SEARCH_PLACEHOLDER')}
value={searchTerm}
onChange={(_, v) => setSearchTerm(v)}
/>
</MenuSearchInput>
</MenuSearch>
<Divider />
<DropdownList>{selectOptions}</DropdownList>
<ScrollableMenuContent maxHeight="50vh">
<MenuSearch>
<MenuSearchInput>
<SearchInput
placeholder={t('TargetContextSelector.SEARCH_PLACEHOLDER')}
value={searchTerm}
onChange={(_, v) => setSearchTerm(v)}
/>
</MenuSearchInput>
</MenuSearch>
<Divider />
<DropdownList>{selectOptions}</DropdownList>
</ScrollableMenuContent>
<MenuFooter>{selectFooter}</MenuFooter>
</Dropdown>
)}
Expand Down
1 change: 1 addition & 0 deletions src/app/TargetView/TargetSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export const TargetSelect: React.FC<TargetSelectProps> = ({ onSelect, simple, ..
<CardBody>
<Dropdown
isScrollable
maxMenuHeight="40vh"
placeholder={t('TargetContextSelector.TOGGLE_PLACEHOLDER')}
isOpen={isDropdownOpen}
onOpenChange={setIsDropdownOpen}
Expand Down
4 changes: 3 additions & 1 deletion src/app/Topology/Actions/NodeActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface ContextMenuItemProps
element: GraphElement | ListElement;
variant: MenuItemVariant;
isDisabled?: (element: GraphElement | ListElement, actionUtils: ActionUtils) => Observable<boolean>;
isDanger?: boolean;
}

export const ContextMenuItem: React.FC<ContextMenuItemProps> = ({
Expand All @@ -51,6 +52,7 @@ export const ContextMenuItem: React.FC<ContextMenuItemProps> = ({
onClick,
variant,
isDisabled,
isDanger,
...props
}) => {
const navigate = useNavigate();
Expand Down Expand Up @@ -102,7 +104,7 @@ export const ContextMenuItem: React.FC<ContextMenuItemProps> = ({
}

return (
<Component {...props} onClick={handleOnclick} isDisabled={disabled}>
<Component {...props} onClick={handleOnclick} isDisabled={disabled} isDanger={!disabled && isDanger}>
{children}
</Component>
);
Expand Down
1 change: 1 addition & 0 deletions src/app/Topology/Actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export interface NodeAction {
readonly action?: NodeActionFunction;
readonly title?: React.ReactNode;
readonly isSeparator?: boolean;
readonly isDanger?: boolean;
readonly isDisabled?: (element: GraphElement | ListElement, actionUtils: ActionUtils) => Observable<boolean>;
readonly allowed?: (element: GraphElement | ListElement) => boolean; // Undefined means allowing all
readonly blocked?: (element: GraphElement | ListElement) => boolean; // Undefined means blocking none
Expand Down
15 changes: 12 additions & 3 deletions src/app/Topology/Actions/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export const nodeActions: NodeAction[] = [
{ key: '', isSeparator: true },
{
key: 'DELETE_TARGET',
isDanger: true,
action: (element, { services }) => {
const targetNode: TargetNode = element.getData();
services.api.deleteTarget(targetNode.target).subscribe(() => undefined);
Expand Down Expand Up @@ -173,7 +174,7 @@ export const nodeActions: NodeAction[] = [
services.api
.graphql<GroupActionResponse>(
`
query DeleteRecordingForGroup ($groupFilter: DiscoveryNodeFilterInput, $recordingFilter: ActiveRecordingsFilterInput) {
query ArchiveRecordingForGroup ($groupFilter: DiscoveryNodeFilterInput, $recordingFilter: ActiveRecordingsFilterInput) {
environmentNodes(filter: $groupFilter) {
name
descendantTargets {
Expand Down Expand Up @@ -258,6 +259,7 @@ export const nodeActions: NodeAction[] = [
key: 'GROUP_DELETE_RECORDING',
title: 'Delete Recording',
isGroup: true,
isDanger: true,
action: (element, { services, notifications }) => {
const group: EnvironmentNode = element.getData();
services.api
Expand Down Expand Up @@ -361,12 +363,19 @@ export const actionFactory = (
}
filtered = stop >= 0 ? filtered.slice(0, stop + 1) : [];

return filtered.map(({ isSeparator, key, title, isDisabled, action }, index) => {
return filtered.map(({ isSeparator, key, title, isDisabled, action, isDanger }, index) => {
if (isSeparator) {
return <ContextMenuSeparator key={`separator-${index}`} />;
}
return (
<ContextMenuItem key={key} element={element} onClick={action} variant={variant} isDisabled={isDisabled}>
<ContextMenuItem
key={key}
element={element}
onClick={action}
variant={variant}
isDisabled={isDisabled}
isDanger={isDanger}
>
{title}
</ContextMenuItem>
);
Expand Down
Loading
Loading