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

Fixes #37675 - Change All Hosts kebab menu to match design #10252

Merged
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
Original file line number Diff line number Diff line change
@@ -1,38 +1,56 @@
import React, { useState } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { Dropdown, KebabToggle } from '@patternfly/react-core';
import { Menu, MenuToggle, Popper } from '@patternfly/react-core';
import { EllipsisVIcon } from '@patternfly/react-icons';

/**
* Generate a button or a dropdown of buttons
* @param {String} title The title of the button for the title and text inside the button
* @param {Object} action action to preform when the button is click can be href with data-method or Onclick
* @return {Function} button component or splitbutton component
*/
export const ActionKebab = ({ items }) => {
const [isOpen, setIsOpen] = useState(false);
export const ActionKebab = ({ items, menuOpen, setMenuOpen }) => {
const containerRef = React.useRef();
if (!items.length) return null;
const menu = (
<Menu
containsFlyout
ouiaId="hosts-index-actions-kebab"
id="hosts-index-actions-kebab"
onSelect={() => setMenuOpen(false)}
>
{items}
</Menu>
);

const menuToggle = (
<MenuToggle
variant="plain"
aria-label="plain kebab"
onClick={() => setMenuOpen(prev => !prev)}
isExpanded={menuOpen}
>
<EllipsisVIcon />
</MenuToggle>
);

return (
<>
{items.length > 0 && (
<Dropdown
ouiaId="action-buttons-dropdown"
toggle={
<KebabToggle
aria-label="toggle action dropdown"
onToggle={setIsOpen}
/>
}
isOpen={isOpen}
isPlain
dropdownItems={items}
/>
)}
</>
<div ref={containerRef}>
<Popper
trigger={menuToggle}
popper={menu}
appendTo={containerRef.current || undefined}
isVisible={menuOpen}
popperMatchesTriggerWidth={false}
/>
</div>
);
};

ActionKebab.propTypes = {
items: PropTypes.arrayOf(PropTypes.node),
menuOpen: PropTypes.bool.isRequired,
setMenuOpen: PropTypes.func.isRequired,
};

ActionKebab.defaultProps = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import forceSingleton from '../../../common/forceSingleton';

const coreTableRowActionsRegistry = forceSingleton(
'coreTableRowActionsRegistry',
() => ({})
);

// Unlike the column registry which is collecting objects that describe table columns,
// here we collect an object containing a single getActions funtion, which returns an array of kebab action items.
export const registerGetActions = ({
pluginName,
getActionsFunc,
tableName = 'hosts',
}) => {
if (!coreTableRowActionsRegistry[pluginName])
coreTableRowActionsRegistry[pluginName] = {};
coreTableRowActionsRegistry[pluginName][tableName] = {
getActions: getActionsFunc,
};
};

export const registeredTableRowActions = ({ tableName = 'hosts' }) => {
const result = {};
Object.keys(coreTableRowActionsRegistry).forEach(pluginName => {
if (coreTableRowActionsRegistry[pluginName]?.[tableName]) {
result[pluginName] = coreTableRowActionsRegistry[pluginName][tableName];
}
});
// { katello: { getActions: [Function: getActions] } }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftovers

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is on purpose, so you know what the object looks like :)

return result;
};

export const getActions = (hostDetailsResult, { tableName = 'hosts' } = {}) => {
const result = [];
const allGetActionsFuncs = registeredTableRowActions({ tableName });
Object.values(allGetActionsFuncs).forEach(
({ getActions: getActionsFunc }) => {
if (typeof getActionsFunc !== 'function') return;
result.push(...getActionsFunc(hostDetailsResult));
}
);
return result;
};

export default getActions;
74 changes: 55 additions & 19 deletions webpack/assets/javascripts/react_app/components/HostsIndex/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { Tr, Td, ActionsColumn } from '@patternfly/react-table';
import {
ToolbarItem,
Divider,
Dropdown,
DropdownItem,
MenuItem,
KebabToggle,
Flex,
FlexItem,
Expand Down Expand Up @@ -52,13 +54,15 @@ import getColumnData from './Columns/core';
import { categoriesFromFrontendColumnData } from '../ColumnSelector/helpers';
import ColumnSelector from '../ColumnSelector';
import { ForemanActionsBarContext } from '../HostDetails/ActionsBar';
import { registerGetActions, getActions } from './TableRowActions/core';

export const ForemanHostsIndexActionsBarContext = forceSingleton(
'ForemanHostsIndexActionsBarContext',
() => createContext({})
);

const HostsIndex = () => {
const [menuOpen, setMenuOpen] = useState(false);
const [allColumns, setAllColumns] = useState(
getColumnData({ tableName: 'hosts' })
);
Expand Down Expand Up @@ -205,55 +209,87 @@ const HostsIndex = () => {
});

const dropdownItems = [
<DropdownItem
ouiaId="delete=hosts-dropdown-item"
key="delete=hosts-dropdown-item"
onClick={handleBulkDelete}
isDisabled={selectedCount === 0}
>
{__('Delete')}
</DropdownItem>,
<DropdownItem
ouiaId="build-hosts-dropdown-item"
<MenuItem
itemId="build-hosts-dropdown-item"
key="build-hosts-dropdown-item"
onClick={setBuildModalOpen}
isDisabled={selectedCount === 0}
>
{__('Build management')}
</DropdownItem>,
<DropdownItem
ouiaId="reassign-hg-dropdown-item"
</MenuItem>,
<MenuItem
itemId="reassign-hg-dropdown-item"
key="reassign-hg-dropdown-item"
onClick={setHgModalOpen}
isDisabled={selectedCount === 0}
>
{__('Change host group')}
</DropdownItem>,
</MenuItem>,
];

const dangerZoneItems = [
<Divider
component="li"
id="danger-zone-separator"
key="danger-zone-separator"
/>,
<MenuItem
itemId="delete=hosts-dropdown-item"
key="delete=hosts-dropdown-item"
onClick={handleBulkDelete}
isDisabled={selectedCount === 0}
>
{__('Delete')}
</MenuItem>,
];

const registeredItems = useSelector(selectKebabItems, shallowEqual);
const pluginToolbarItems = jsReady => (
<ForemanHostsIndexActionsBarContext.Provider
value={{ ...selectAllOptions, fetchBulkParams }}
value={{ ...selectAllOptions, fetchBulkParams, menuOpen, setMenuOpen }}
>
<ActionKebab items={dropdownItems.concat(registeredItems)} />
<ActionKebab
items={dropdownItems.concat(registeredItems).concat(dangerZoneItems)}
menuOpen={menuOpen}
setMenuOpen={setMenuOpen}
/>
{jsReady && <ColumnSelector data={columnSelectData} />}
</ForemanHostsIndexActionsBarContext.Provider>
);

const rowKebabItems = ({
const coreRowKebabItems = ({
id,
name: hostName,
compute_id: computeId,
can_delete: canDelete,
can_edit: canEdit,
}) => [
{
title: __('Edit'),
onClick: () => {
window.location.href = foremanUrl(`/hosts/${id}/edit`);
},
isDisabled: !canEdit,
},
{
title: __('Clone'),
onClick: () => {
window.location.href = foremanUrl(`/hosts/${id}/clone`);
},
isDisabled: !canEdit,
},
{
title: __('Delete'),
onClick: () => deleteHostHandler({ id, hostName, computeId }),
isDisabled: !canDelete,
},
];

registerGetActions({
pluginName: 'core',
getActionsFunc: coreRowKebabItems,
});

const [legacyUIKebabOpen, setLegacyUIKebabOpen] = useState(false);
const legacyUIKebab = (
<Dropdown
Expand Down Expand Up @@ -345,7 +381,7 @@ const HostsIndex = () => {
ouiaId="hosts-index-table"
params={params}
setParams={setParamsAndAPI}
getActions={rowKebabItems}
getActions={getActions}
itemCount={subtotal}
results={results}
url={HOSTS_API_PATH}
Expand All @@ -364,7 +400,7 @@ const HostsIndex = () => {
isPending={status === STATUS.PENDING}
>
{results?.map((result, rowIndex) => {
const rowActions = rowKebabItems(result);
const rowActions = getActions(result);
return (
<Tr key={rowIndex} ouiaId={`table-row-${rowIndex}`} isHoverable>
{<RowSelectTd rowData={result} {...{ selectOne, isSelected }} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,22 @@ export const Table = ({
setSelectedItem({ id, name });
setDeleteModalOpen(true);
};
const actions = ({ can_delete: canDelete, id, name, ...item }) =>
const actions = ({
can_delete: canDelete,
can_edit: canEdit,
id,
name,
...item
}) =>
[
isDeleteable && {
title: __('Delete'),
onClick: () => onDeleteClick({ id, name }),
isDisabled: !canDelete,
},
...((getActions && getActions({ id, name, canDelete, ...item })) ?? []),
...((getActions &&
getActions({ id, name, canDelete, canEdit, ...item })) ??
[]),
].filter(Boolean);
const RowSelectTd = rowSelectTd;
return (
Expand Down
Loading