forked from freeipa/freeipa-webui
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Issue 401 - Implement HBAC rules page
Fixes: freeipa#401 Signed-off-by: Mark Reynolds <[email protected]>
- Loading branch information
1 parent
a919c78
commit f4a3afa
Showing
11 changed files
with
2,216 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,277 @@ | ||
/* eslint-disable @typescript-eslint/no-unused-vars */ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import React, { FormEvent, useEffect, useRef, useState } from "react"; | ||
// PatternFly | ||
import { | ||
Menu, | ||
MenuContent, | ||
MenuItem, | ||
MenuList, | ||
MenuToggle, | ||
MenuToggleCheckbox, | ||
} from "@patternfly/react-core"; | ||
// Data types | ||
import { HBACRule } from "src/utils/datatypes/globalDataTypes"; | ||
// Layouts | ||
import BulkSelectorLayout from "src/components/layouts/BulkSelectorLayout"; | ||
|
||
interface RulesData { | ||
selectedRules: HBACRule[]; | ||
selectableRulesTable: HBACRule[]; | ||
isHbacRuleSelectable: (rule: HBACRule) => boolean; | ||
updateSelectedRules: (rule: HBACRule[], isSelected: boolean) => void; | ||
} | ||
|
||
interface ButtonsData { | ||
updateIsDeleteButtonDisabled: (value: boolean) => void; | ||
} | ||
|
||
interface SelectedPerPageData { | ||
selectedPerPage: number; | ||
updateSelectedPerPage: (selected: number) => void; | ||
} | ||
|
||
interface PropsToBulkSelectorPrep { | ||
list: HBACRule[]; | ||
shownElementsList: HBACRule[]; | ||
elementData: RulesData; | ||
buttonsData: ButtonsData; | ||
selectedPerPageData: SelectedPerPageData; | ||
} | ||
|
||
const BulkSelectorHBACRulesPrep = (props: PropsToBulkSelectorPrep) => { | ||
// Table functionality (from parent component) to manage the bulk selector functionality | ||
// - Menu | ||
const [isOpenMenu, setIsOpenMenu] = useState(false); | ||
const toggleRefMenu = useRef<any>(null); | ||
const menuRef = useRef<HTMLDivElement>(null); | ||
const containerRefMenu = useRef<HTMLDivElement>(null); | ||
|
||
const handleMenuKeys = (event: KeyboardEvent) => { | ||
if (!isOpenMenu) { | ||
return; | ||
} | ||
if ( | ||
menuRef.current?.contains(event.target as Node) || | ||
toggleRefMenu.current?.contains(event.target as Node) | ||
) { | ||
if (event.key === "Escape" || event.key === "Tab") { | ||
setIsOpenMenu(!isOpenMenu); | ||
toggleRefMenu.current?.focus(); | ||
} | ||
} | ||
}; | ||
|
||
const handleClickOutside = (event: MouseEvent) => { | ||
if (isOpenMenu && !menuRef.current?.contains(event.target as Node)) { | ||
setIsOpenMenu(false); | ||
} | ||
}; | ||
|
||
React.useEffect(() => { | ||
window.addEventListener("keydown", handleMenuKeys); | ||
window.addEventListener("click", handleClickOutside); | ||
return () => { | ||
window.removeEventListener("keydown", handleMenuKeys); | ||
window.removeEventListener("click", handleClickOutside); | ||
}; | ||
}, [isOpenMenu, menuRef]); | ||
|
||
// - When a bulk selector element is selected, it remains highlighted | ||
const onToggleClick = (ev: React.MouseEvent) => { | ||
ev.stopPropagation(); // Stop handleClickOutside from handling | ||
setIsOpenMenu(!isOpenMenu); | ||
}; | ||
|
||
// - Selectable checkboxes on table (elements per page) | ||
const selectableElementsPage = props.shownElementsList.filter( | ||
props.elementData.isHbacRuleSelectable | ||
); | ||
|
||
// - Methods to manage the Bulk selector options | ||
// -- Unselect all items on the table | ||
const unselectPageItems = () => { | ||
props.elementData.updateSelectedRules(props.shownElementsList, false); | ||
}; | ||
|
||
const unselectAllItems = () => { | ||
props.elementData.updateSelectedRules( | ||
props.elementData.selectedRules, | ||
false | ||
); | ||
props.buttonsData.updateIsDeleteButtonDisabled(true); | ||
}; | ||
|
||
// Select all elements (Page) | ||
const selectAllElementsPage = ( | ||
isSelecting = true, | ||
selectableRulesList: HBACRule[] | ||
) => { | ||
// Enable/disable 'Delete' button | ||
if (isSelecting) { | ||
props.elementData.updateSelectedRules(selectableRulesList, true); | ||
|
||
// Enable delete button | ||
props.buttonsData.updateIsDeleteButtonDisabled(false); | ||
// Update the 'selectedPerPage' counter | ||
props.selectedPerPageData.updateSelectedPerPage( | ||
selectableRulesList.length | ||
); | ||
} else { | ||
props.elementData.updateSelectedRules(props.shownElementsList, false); | ||
props.buttonsData.updateIsDeleteButtonDisabled(true); | ||
// Restore the 'selectedPerPage' counter | ||
props.selectedPerPageData.updateSelectedPerPage(0); | ||
} | ||
}; | ||
|
||
// Helper method to manage the checkbox icon symbol | ||
// - All rows selected: true (full check) | ||
// - Some rows selected: null (-) | ||
// - None selected: false (empty) | ||
const areAllElementsSelected: boolean | null = | ||
props.elementData.selectedRules.length > 0 && | ||
props.elementData.selectedRules.length === | ||
props.elementData.selectableRulesTable.length | ||
? true | ||
: props.elementData.selectedRules.length > 0 | ||
? null | ||
: false; | ||
|
||
// Menu toggle element with checkbox | ||
const toggle = ( | ||
<MenuToggle | ||
ref={toggleRefMenu} | ||
onClick={onToggleClick} | ||
isExpanded={isOpenMenu} | ||
splitButtonOptions={{ | ||
items: [ | ||
<MenuToggleCheckbox | ||
id="split-button-checkbox-with-text-disabled-example" | ||
key="split-checkbox-with-text-disabled" | ||
aria-label="Select all" | ||
onChange={( | ||
isSelecting: boolean | undefined, | ||
event: FormEvent<HTMLInputElement> | ||
) => | ||
selectAllElementsPage( | ||
isSelecting, | ||
props.elementData.selectableRulesTable | ||
) | ||
} | ||
isChecked={areAllElementsSelected} | ||
> | ||
{props.elementData.selectedRules.length > 0 && ( | ||
<p>{props.elementData.selectedRules.length + " selected"}</p> | ||
)} | ||
</MenuToggleCheckbox>, | ||
], | ||
}} | ||
aria-label="Menu toggle with checkbox split button and text" | ||
/> | ||
); | ||
|
||
// Checks wether all the elements on the currect page have been selected or not | ||
const [currentPageAlreadySelected, setCurrentPageAlreadySelected] = | ||
useState(false); | ||
|
||
// The 'currentPageAlreadySelected' should be set when elements are selected | ||
useEffect(() => { | ||
const found = props.shownElementsList.every((group) => | ||
props.elementData.selectedRules.find( | ||
(selectedGroup) => selectedGroup.cn === group.cn | ||
) | ||
); | ||
|
||
if (found) { | ||
// All the elements on that page are been selected | ||
setCurrentPageAlreadySelected(true); | ||
} else { | ||
// The elements on that page are not been selected (yet) | ||
setCurrentPageAlreadySelected(false); | ||
// If there is no elements selected on the page yet, reset 'selectedPerPage' | ||
if ( | ||
!props.shownElementsList.some((group) => | ||
props.elementData.selectedRules.find( | ||
(selectedGroup) => selectedGroup.cn === group.cn | ||
) | ||
) | ||
) { | ||
props.selectedPerPageData.updateSelectedPerPage(0); | ||
} | ||
} | ||
}, [props.elementData.selectedRules.length, props.shownElementsList]); | ||
|
||
// Set the messages displayed in the 'Select page' option (bulk selector) | ||
const getSelectedElements = () => { | ||
let msg = | ||
"Select page (" + props.elementData.selectedRules.length + " items)"; | ||
const remainingElements = Math.min( | ||
props.elementData.selectedRules.length + | ||
props.shownElementsList.length - | ||
props.selectedPerPageData.selectedPerPage, | ||
props.list.length | ||
); | ||
|
||
if ( | ||
props.list.length > props.elementData.selectedRules.length && | ||
!currentPageAlreadySelected | ||
) { | ||
msg = "Select page (" + remainingElements + " items)"; | ||
} | ||
|
||
return msg; | ||
}; | ||
|
||
const group_id_list = props.elementData.selectedRules.map((group) => { | ||
return group.cn; | ||
}); | ||
|
||
// Menu options | ||
const menuToolbar = ( | ||
<Menu | ||
ref={menuRef} | ||
style={{ minWidth: "fit-content" }} | ||
onSelect={(_ev) => { | ||
setIsOpenMenu(!isOpenMenu); | ||
toggleRefMenu.current?.querySelector("button").focus(); | ||
}} | ||
> | ||
<MenuContent> | ||
<MenuList> | ||
<MenuItem itemId={0} onClick={unselectPageItems}> | ||
Unselect page (0 items) | ||
</MenuItem> | ||
<MenuItem itemId={1} onClick={unselectAllItems}> | ||
Unselect all (0 items) | ||
</MenuItem> | ||
<MenuItem | ||
itemId={2} | ||
onClick={() => selectAllElementsPage(true, selectableElementsPage)} | ||
// NOTE: The line below disables this BS option when all the page rows have been selected. | ||
// This can benefit the user experience as it provides extra context of the already selected elements. | ||
// isDisabled={currentPageAlreadySelected} | ||
> | ||
{getSelectedElements()} | ||
</MenuItem> | ||
</MenuList> | ||
</MenuContent> | ||
</Menu> | ||
); | ||
|
||
// Renders component with the elements' data | ||
return ( | ||
<BulkSelectorLayout | ||
menuKey="menu-all-groups-table" | ||
containerRefMenu={containerRefMenu} | ||
toggle={toggle} | ||
menuToolbar={menuToolbar} | ||
appendTo={containerRefMenu.current || undefined} | ||
isOpenMenu={isOpenMenu} | ||
ariaLabel="Menu toggle with checkbox split button" | ||
title={group_id_list.join(", ")} | ||
/> | ||
); | ||
}; | ||
|
||
export default BulkSelectorHBACRulesPrep; |
Oops, something went wrong.