Skip to content

Commit 6a35cad

Browse files
Merge pull request #15028 from logonoff/OCPBUGS-55896-actions
OCPBUGS-55896: Refactor Kebab+ActionsMenu to use PF
2 parents 5f50f62 + ba2e273 commit 6a35cad

File tree

11 files changed

+209
-421
lines changed

11 files changed

+209
-421
lines changed

frontend/packages/integration-tests-cypress/tests/cluster-settings/alertmanager/alertmanager.cy.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ receivers:
105105
.within(() => {
106106
cy.get('[data-test-id="kebab-button"]').click();
107107
});
108-
cy.get('[data-test-action="Delete Receiver"]').should('be.disabled');
108+
cy.get('[data-test-action="Delete Receiver"] button').should('be.disabled');
109109
alertmanager.reset();
110110
});
111111

frontend/packages/pipelines-plugin/src/components/pipelines/pipeline-builder/task-sidebar/TaskSidebarHeader.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import { useTranslation } from 'react-i18next';
3-
import { ActionsMenu } from '@console/internal/components/utils';
3+
import { ActionsMenu } from '@console/internal/components/utils/actions-menu';
44
import { PageHeading } from '@console/shared/src/components/heading/PageHeading';
55
import { TaskKind } from '../../../../types';
66
import PipelineResourceRef from '../../../shared/common/PipelineResourceRef';

frontend/packages/topology/src/components/side-bar/TopologyEdgePanel.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import * as React from 'react';
55
import { Edge, isNode, Node } from '@patternfly/react-topology';
66
import { useTranslation } from 'react-i18next';
7-
import { ActionsMenu, SimpleTabNav } from '@console/internal/components/utils';
7+
import { SimpleTabNav } from '@console/internal/components/utils';
8+
import { ActionsMenu } from '@console/internal/components/utils/actions-menu';
89
import { PageHeading } from '@console/shared/src/components/heading/PageHeading';
910
import { edgeActions } from '../../actions/edgeActions';
1011
import { TYPE_TRAFFIC_CONNECTOR } from '../../const';

frontend/public/components/utils/__tests__/kebab.spec.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,21 @@ describe('KebabItem', () => {
1818
const nothingOption = { ...mockOption, href: undefined };
1919
const trackOnClick = jest.fn();
2020
const renderItem = render(<KebabItem onClick={trackOnClick} option={nothingOption} />);
21-
fireEvent.click(renderItem.getByRole('button'));
21+
fireEvent.click(renderItem.getByRole('menuitem'));
2222
expect(trackOnClick).toHaveBeenCalledTimes(0);
2323
});
2424
it('should enable when option has href', () => {
2525
const hrefOption = { ...mockOption };
2626
const trackOnClick = jest.fn();
2727
const renderItem = render(<KebabItem onClick={trackOnClick} option={hrefOption} />);
28-
fireEvent.click(renderItem.getByRole('button'));
28+
fireEvent.click(renderItem.getByRole('menuitem'));
2929
expect(trackOnClick).toHaveBeenCalledTimes(1);
3030
});
3131
it('should enable when option has a callback', () => {
3232
const callbackOption = { ...mockOption, href: undefined, callback: () => {} };
3333
const trackOnClick = jest.fn();
3434
const renderItem = render(<KebabItem onClick={trackOnClick} option={callbackOption} />);
35-
fireEvent.click(renderItem.getByRole('button'));
35+
fireEvent.click(renderItem.getByRole('menuitem'));
3636
expect(trackOnClick).toHaveBeenCalledTimes(1);
3737
});
3838
});
@@ -55,7 +55,7 @@ describe('KebabItemAccessReview_', () => {
5555
impersonate={mockImpersonate}
5656
/>,
5757
);
58-
fireEvent.click(renderItem.getByRole('button'));
58+
fireEvent.click(renderItem.getByRole('menuitem'));
5959
expect(trackOnClick).toHaveBeenCalledTimes(0);
6060
});
6161
it('should enable option when option.accessReview present and allowed', () => {
@@ -69,7 +69,7 @@ describe('KebabItemAccessReview_', () => {
6969
impersonate={mockImpersonate}
7070
/>,
7171
);
72-
fireEvent.click(renderItem.getByRole('button'));
72+
fireEvent.click(renderItem.getByRole('menuitem'));
7373
expect(trackOnClick).toHaveBeenCalledTimes(1);
7474
});
7575
});

frontend/public/components/utils/_kebab.scss

-21
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {
2+
ImpersonateKind,
3+
impersonateStateToProps,
4+
useSafetyFirst,
5+
} from '@console/dynamic-plugin-sdk';
6+
import { Dropdown, MenuToggle, MenuToggleElement } from '@patternfly/react-core';
7+
import { some } from 'lodash-es';
8+
import { useTranslation } from 'react-i18next';
9+
import { connect } from 'react-redux';
10+
import { useNavigate } from 'react-router-dom-v5-compat';
11+
12+
import { ReactNode, RefObject, useEffect, useState } from 'react';
13+
import { KebabItems, KebabOption } from './kebab';
14+
import { checkAccess } from './rbac';
15+
16+
type ActionsMenuProps = {
17+
actions: KebabOption[];
18+
title?: ReactNode;
19+
};
20+
21+
const ActionsMenuDropdown = (props) => {
22+
const { t } = useTranslation();
23+
const navigate = useNavigate();
24+
const [active, setActive] = useState(!!props.active);
25+
26+
const onClick = (event, option) => {
27+
event.preventDefault();
28+
29+
if (option.callback) {
30+
option.callback();
31+
}
32+
33+
if (option.href) {
34+
navigate(option.href);
35+
}
36+
37+
setActive(false);
38+
};
39+
40+
return (
41+
<Dropdown
42+
isOpen={active}
43+
onSelect={() => setActive(false)}
44+
onOpenChange={setActive}
45+
shouldFocusToggleOnSelect
46+
popperProps={{
47+
enableFlip: true,
48+
position: 'right',
49+
}}
50+
toggle={(toggleRef: RefObject<MenuToggleElement>) => (
51+
<MenuToggle
52+
ref={toggleRef}
53+
onClick={() => setActive(!active)}
54+
isExpanded={active}
55+
data-test-id="actions-menu-button"
56+
>
57+
{props.title || t('public~Actions')}
58+
</MenuToggle>
59+
)}
60+
>
61+
<KebabItems options={props.actions} onClick={onClick} />
62+
</Dropdown>
63+
);
64+
};
65+
66+
export const ActionsMenu: React.FCC<ActionsMenuProps> = connect(impersonateStateToProps)(
67+
({
68+
actions,
69+
impersonate,
70+
title = undefined,
71+
}: ActionsMenuProps & { impersonate?: ImpersonateKind }) => {
72+
const [isVisible, setVisible] = useSafetyFirst(false);
73+
74+
// Check if any actions are visible when actions have access reviews.
75+
useEffect(() => {
76+
if (!actions.length) {
77+
setVisible(false);
78+
return;
79+
}
80+
const promises = actions.reduce((acc, action) => {
81+
if (action.accessReview) {
82+
acc.push(checkAccess(action.accessReview));
83+
}
84+
return acc;
85+
}, []);
86+
87+
// Only need to resolve if all actions require access review
88+
if (promises.length !== actions.length) {
89+
setVisible(true);
90+
return;
91+
}
92+
Promise.all(promises)
93+
.then((results) => setVisible(some(results, 'status.allowed')))
94+
.catch(() => setVisible(true));
95+
}, [actions, impersonate, setVisible]);
96+
return isVisible ? <ActionsMenuDropdown actions={actions} title={title} /> : null;
97+
},
98+
);

frontend/public/components/utils/dropdown.jsx

+2-152
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,12 @@ import * as _ from 'lodash-es';
22
import * as React from 'react';
33
import classNames from 'classnames';
44
import * as PropTypes from 'prop-types';
5-
import { connect } from 'react-redux';
6-
import { useTranslation, withTranslation } from 'react-i18next';
7-
import { useNavigate } from 'react-router-dom-v5-compat';
8-
import { Divider, Popper, Title } from '@patternfly/react-core';
9-
import { impersonateStateToProps, useSafetyFirst } from '@console/dynamic-plugin-sdk';
105
import { useUserSettingsCompatibility } from '@console/shared';
6+
import { Divider, Popper, Title } from '@patternfly/react-core';
117
import { CaretDownIcon } from '@patternfly/react-icons/dist/esm/icons/caret-down-icon';
128
import { CheckIcon } from '@patternfly/react-icons/dist/esm/icons/check-icon';
139
import { StarIcon } from '@patternfly/react-icons/dist/esm/icons/star-icon';
14-
15-
import { checkAccess } from './rbac';
16-
import { KebabItems } from './kebab';
10+
import { withTranslation } from 'react-i18next';
1711

1812
class DropdownMixin extends React.PureComponent {
1913
constructor(props) {
@@ -678,147 +672,3 @@ Dropdown.propTypes = {
678672
required: PropTypes.bool,
679673
dataTest: PropTypes.string,
680674
};
681-
682-
const ActionsMenuDropdown = (props) => {
683-
const { t } = useTranslation();
684-
const navigate = useNavigate();
685-
const [active, setActive] = React.useState(!!props.active);
686-
687-
const dropdownElement = React.useRef();
688-
689-
const show = () => {
690-
setActive(true);
691-
};
692-
693-
const hide = (e) => {
694-
e?.stopPropagation();
695-
setActive(false);
696-
};
697-
698-
const listener = React.useCallback(
699-
(event) => {
700-
if (!active) {
701-
return;
702-
}
703-
704-
const { current } = dropdownElement;
705-
if (!current) {
706-
return;
707-
}
708-
709-
if (event.target === current || current.contains(event.target)) {
710-
return;
711-
}
712-
713-
hide(event);
714-
},
715-
[active, dropdownElement],
716-
);
717-
718-
React.useEffect(() => {
719-
if (active) {
720-
window.addEventListener('click', listener);
721-
} else {
722-
window.removeEventListener('click', listener);
723-
}
724-
return () => {
725-
window.removeEventListener('click', listener);
726-
};
727-
}, [active, listener]);
728-
729-
const toggle = (e) => {
730-
e.preventDefault();
731-
732-
if (props.disabled) {
733-
return;
734-
}
735-
736-
if (active) {
737-
hide(e);
738-
} else {
739-
show(e);
740-
}
741-
};
742-
743-
const onClick = (event, option) => {
744-
event.preventDefault();
745-
746-
if (option.callback) {
747-
option.callback();
748-
}
749-
750-
if (option.href) {
751-
navigate(option.href);
752-
}
753-
754-
hide();
755-
};
756-
757-
return (
758-
<div ref={dropdownElement}>
759-
<button
760-
type="button"
761-
aria-haspopup="true"
762-
aria-label={t('public~Actions')}
763-
aria-expanded={active}
764-
className={classNames({
765-
'pf-v6-c-menu-toggle': true,
766-
'pf-m-expanded': active,
767-
})}
768-
onClick={toggle}
769-
data-test-id="actions-menu-button"
770-
>
771-
<span className="pf-v6-c-menu__toggle-text">{props.title || t('public~Actions')}</span>
772-
<CaretDownIcon />
773-
</button>
774-
{active && (
775-
<div className="co-actions-menu dropdown-menu pf-v6-c-menu">
776-
<KebabItems options={props.actions} onClick={onClick} />
777-
</div>
778-
)}
779-
</div>
780-
);
781-
};
782-
783-
const ActionsMenu_ = ({ actions, impersonate, title = undefined }) => {
784-
const [isVisible, setVisible] = useSafetyFirst(false);
785-
786-
// Check if any actions are visible when actions have access reviews.
787-
React.useEffect(() => {
788-
if (!actions.length) {
789-
setVisible(false);
790-
return;
791-
}
792-
const promises = actions.reduce((acc, action) => {
793-
if (action.accessReview) {
794-
acc.push(checkAccess(action.accessReview));
795-
}
796-
return acc;
797-
}, []);
798-
799-
// Only need to resolve if all actions require access review
800-
if (promises.length !== actions.length) {
801-
setVisible(true);
802-
return;
803-
}
804-
Promise.all(promises)
805-
.then((results) => setVisible(_.some(results, 'status.allowed')))
806-
.catch(() => setVisible(true));
807-
}, [actions, impersonate, setVisible]);
808-
return isVisible ? <ActionsMenuDropdown actions={actions} title={title} /> : null;
809-
};
810-
811-
export const ActionsMenu = connect(impersonateStateToProps)(ActionsMenu_);
812-
813-
ActionsMenu.propTypes = {
814-
actions: PropTypes.arrayOf(
815-
PropTypes.shape({
816-
label: PropTypes.node,
817-
labelKey: PropTypes.string,
818-
href: PropTypes.string,
819-
callback: PropTypes.func,
820-
accessReview: PropTypes.object,
821-
}),
822-
).isRequired,
823-
title: PropTypes.node,
824-
};

frontend/public/components/utils/headings.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import * as React from 'react';
88
import { useTranslation } from 'react-i18next';
99

1010
import { PageHeading, PageHeadingProps } from '@console/shared/src/components/heading/PageHeading';
11+
import { ActionsMenu } from '@console/internal/components/utils/actions-menu';
1112
import { connectToModel } from '../../kinds';
1213
import { K8sKind, K8sResourceKind, K8sResourceKindReference } from '../../module/k8s';
13-
import { ActionsMenu, FirehoseResult, KebabOption, ResourceIcon } from './index';
14+
import { FirehoseResult, KebabOption, ResourceIcon } from './index';
1415
import { ManagedByOperatorLink } from './managed-by';
1516

1617
export const ResourceItemDeleting = () => {

frontend/public/components/utils/index.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export * from './dom-utils';
5353
export * from './owner-references';
5454
export { default } from './operator-backed-owner-references';
5555
export * from './field-level-help';
56-
export * from './single-typeahead-dropdown';
5756
export * from './details-item';
5857
export * from './types';
5958
export * from './release-notes-link';

0 commit comments

Comments
 (0)