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.
The `UseMemberOf`component is managing multiple data types and its functionality is cumbersome and difficult to maintain. A refactor for the different tabs (User groups, Netgroups, etc.) can be benfitial as that functionality can be handled in different components and wrappers. Signed-off-by: Carla Martinez <[email protected]>
- Loading branch information
Showing
5 changed files
with
440 additions
and
130 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,107 @@ | ||
import React from "react"; | ||
// PatternFly | ||
import { Table, Tr, Th, Td, Thead, Tbody } from "@patternfly/react-table"; | ||
// Data types | ||
import { UserGroup } from "src/utils/datatypes/globalDataTypes"; | ||
// Components | ||
import SkeletonOnTableLayout from "../layouts/Skeleton/SkeletonOnTableLayout"; | ||
import EmptyBodyTable from "../tables/EmptyBodyTable"; | ||
|
||
export interface MemberOfUserGroupsTableProps { | ||
userGroups: UserGroup[]; | ||
checkedItems: string[]; | ||
onCheckItemsChange: (checkedItems: string[]) => void; | ||
showTableRows: boolean; | ||
} | ||
|
||
// Body | ||
const UserGroupsTableBody = (props: { | ||
userGroups: UserGroup[]; | ||
checkedItems: string[]; | ||
onCheckboxChange: (checked: boolean, groupName: string) => void; | ||
}) => { | ||
const { userGroups } = props; | ||
return ( | ||
<> | ||
{userGroups.map((userGroup, index) => ( | ||
<Tr key={index}> | ||
<Td | ||
select={{ | ||
rowIndex: index, | ||
onSelect: (_e, isSelected) => | ||
props.onCheckboxChange(isSelected, userGroup.name), | ||
isSelected: props.checkedItems.includes(userGroup.name), | ||
}} | ||
/> | ||
<Td>{userGroup.name}</Td> | ||
<Td>{userGroup.gid}</Td> | ||
<Td>{userGroup.description}</Td> | ||
</Tr> | ||
))} | ||
</> | ||
); | ||
}; | ||
|
||
// Define skeleton | ||
const skeleton = ( | ||
<SkeletonOnTableLayout | ||
rows={4} | ||
colSpan={4} | ||
screenreaderText={"Loading table rows"} | ||
/> | ||
); | ||
|
||
export default function MemberOfUserGroupsTable( | ||
props: MemberOfUserGroupsTableProps | ||
) { | ||
const { userGroups } = props; | ||
if (!userGroups || userGroups.length <= 0) { | ||
return null; // return empty placeholder | ||
} | ||
|
||
const onCheckboxChange = (checked: boolean, groupName: string) => { | ||
let newItems: string[] = []; | ||
if (checked) { | ||
newItems = [...props.checkedItems, groupName]; | ||
} else { | ||
newItems = props.checkedItems.filter((name) => name !== groupName); | ||
} | ||
if (props.onCheckItemsChange) { | ||
props.onCheckItemsChange(newItems); | ||
} | ||
}; | ||
|
||
return ( | ||
<Table | ||
aria-label="member of table" | ||
name="cn" | ||
variant="compact" | ||
borders | ||
className={"pf-v5-u-mt-md"} | ||
id="member-of-table" | ||
isStickyHeader | ||
> | ||
<Thead> | ||
<Tr> | ||
<Th /> | ||
<Th modifier="wrap">Group name</Th> | ||
<Th modifier="wrap">GID</Th> | ||
<Th modifier="wrap">Description</Th> | ||
</Tr> | ||
</Thead> | ||
<Tbody> | ||
{!props.showTableRows ? ( | ||
skeleton | ||
) : userGroups.length === 0 ? ( | ||
<EmptyBodyTable /> | ||
) : ( | ||
<UserGroupsTableBody | ||
userGroups={userGroups} | ||
onCheckboxChange={onCheckboxChange} | ||
checkedItems={props.checkedItems} | ||
/> | ||
)} | ||
</Tbody> | ||
</Table> | ||
); | ||
} |
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,192 @@ | ||
import React from "react"; | ||
// PatternFly | ||
import { | ||
Button, | ||
Form, | ||
FormGroup, | ||
Pagination, | ||
Text, | ||
TextContent, | ||
TextVariants, | ||
ToggleGroup, | ||
ToggleGroupItem, | ||
Toolbar, | ||
ToolbarContent, | ||
ToolbarItem, | ||
ToolbarItemVariant, | ||
} from "@patternfly/react-core"; | ||
// Icons | ||
import OutlinedQuestionCircleIcon from "@patternfly/react-icons/dist/esm/icons/outlined-question-circle-icon"; | ||
// Components | ||
import SearchInputLayout from "../layouts/SearchInputLayout"; | ||
|
||
export type MembershipDirection = "direct" | "indirect"; | ||
|
||
interface MemberOfToolbarProps { | ||
// search | ||
searchText: string; | ||
onSearchTextChange: (value: string) => void; | ||
|
||
// buttons | ||
refreshButtonEnabled: boolean; | ||
onRefreshButtonClick?: () => void; | ||
deleteButtonEnabled: boolean; | ||
onDeleteButtonClick?: () => void; | ||
addButtonEnabled: boolean; | ||
onAddButtonClick?: () => void; | ||
|
||
membershipDirectionEnabled?: boolean; | ||
membershipDirection?: MembershipDirection; | ||
onMembershipDirectionChange: (direction: MembershipDirection) => void; | ||
|
||
// help icon | ||
helpIconEnabled?: boolean; | ||
onHelpIconClick?: () => void; | ||
|
||
// paging | ||
totalItems: number; | ||
perPage: number; | ||
page: number; | ||
onPageChange?: (page: number) => void; | ||
onPerPageChange?: (pageSize: number) => void; | ||
} | ||
|
||
const MemberOfToolbar = (props: MemberOfToolbarProps) => { | ||
const onMembershipDirectionChange = ( | ||
selected, | ||
direction: MembershipDirection | ||
) => { | ||
if (selected && props.onMembershipDirectionChange) { | ||
props.onMembershipDirectionChange(direction); | ||
} | ||
}; | ||
|
||
return ( | ||
<Toolbar> | ||
<ToolbarContent> | ||
<ToolbarItem | ||
id="search-input" | ||
variant={ToolbarItemVariant["search-filter"]} | ||
spacer={{ default: "spacerMd" }} | ||
> | ||
<SearchInputLayout | ||
name="search" | ||
ariaLabel="Search user" | ||
placeholder="Search" | ||
searchValueData={{ | ||
searchValue: props.searchText, | ||
updateSearchValue: props.onSearchTextChange, | ||
}} | ||
/> | ||
</ToolbarItem> | ||
<ToolbarItem | ||
id="separator-refresh" | ||
variant={ToolbarItemVariant.separator} | ||
/> | ||
<ToolbarItem id="refresh-button"> | ||
<Button | ||
variant="secondary" | ||
name="refresh" | ||
isDisabled={!props.refreshButtonEnabled} | ||
onClick={props.onRefreshButtonClick} | ||
> | ||
Refresh | ||
</Button> | ||
</ToolbarItem> | ||
<ToolbarItem id="delete-button"> | ||
<Button | ||
variant="secondary" | ||
name="remove" | ||
isDisabled={!props.deleteButtonEnabled} | ||
onClick={props.onDeleteButtonClick} | ||
> | ||
Delete | ||
</Button> | ||
</ToolbarItem> | ||
<ToolbarItem id="add-button"> | ||
<Button | ||
variant="secondary" | ||
name="add" | ||
isDisabled={!props.addButtonEnabled} | ||
onClick={props.onAddButtonClick} | ||
> | ||
Add | ||
</Button> | ||
</ToolbarItem> | ||
<ToolbarItem | ||
id="separator-membership" | ||
variant={ToolbarItemVariant.separator} | ||
/> | ||
<ToolbarItem id="membership-form"> | ||
<Form isHorizontal maxWidth="93px" className="pf-v5-u-pb-xs"> | ||
<FormGroup | ||
fieldId="membership" | ||
label="Membership" | ||
className="pf-v5-u-pt-0" | ||
></FormGroup> | ||
</Form> | ||
</ToolbarItem> | ||
<ToolbarItem id="toggle-group"> | ||
<ToggleGroup | ||
isCompact | ||
aria-label="Toggle group with single selectable" | ||
> | ||
<ToggleGroupItem | ||
text="Direct" | ||
name="user-memberof-group-type-radio-direct" | ||
buttonId="direct" | ||
isSelected={props.membershipDirection === "direct"} | ||
onChange={(_event, selected) => | ||
onMembershipDirectionChange(selected, "direct") | ||
} | ||
/> | ||
<ToggleGroupItem | ||
text="Indirect" | ||
name="user-memberof-group-type-radio-indirect" | ||
buttonId="indirect" | ||
isSelected={props.membershipDirection === "indirect"} | ||
onChange={(_event, selected) => | ||
onMembershipDirectionChange(selected, "indirect") | ||
} | ||
/> | ||
</ToggleGroup> | ||
</ToolbarItem> | ||
<ToolbarItem | ||
id="separator-help-icon" | ||
variant={ToolbarItemVariant.separator} | ||
/> | ||
<ToolbarItem id="help-icon"> | ||
<> | ||
{props.helpIconEnabled && ( | ||
<TextContent> | ||
<Text component={TextVariants.p}> | ||
<OutlinedQuestionCircleIcon className="pf-v5-u-primary-color-100 pf-v5-u-mr-sm" /> | ||
<Text component={TextVariants.a} isVisitedLink> | ||
Help | ||
</Text> | ||
</Text> | ||
</TextContent> | ||
)} | ||
</> | ||
</ToolbarItem> | ||
<ToolbarItem id="pagination" align={{ default: "alignRight" }}> | ||
<Pagination | ||
itemCount={props.totalItems} | ||
perPage={props.perPage} | ||
page={props.page} | ||
onSetPage={(_e, page) => | ||
props.onPageChange ? props.onPageChange(page) : null | ||
} | ||
widgetId="pagination-options-menu-top" | ||
onPerPageSelect={(_e, perPage) => | ||
props.onPerPageChange ? props.onPerPageChange(perPage) : null | ||
} | ||
isCompact | ||
/> | ||
</ToolbarItem> | ||
</ToolbarContent> | ||
</Toolbar> | ||
); | ||
}; | ||
|
||
export default MemberOfToolbar; |
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,86 @@ | ||
import React from "react"; | ||
// Repositories | ||
import { userGroupsInitialData } from "src/utils/data/GroupRepositories"; | ||
// Data types | ||
import { UserGroup } from "src/utils/datatypes/globalDataTypes"; | ||
// Components | ||
import MemberOfToolbarUserGroups, { | ||
MembershipDirection, | ||
} from "./MemberOfToolbar"; | ||
import MemberOfUserGroupsTable from "./MemberOfTableUserGroups"; | ||
import { Pagination, PaginationVariant } from "@patternfly/react-core"; | ||
|
||
interface MemberOfUserGroupsProps { | ||
showAddModal: () => void; | ||
showDeleteModal: () => void; | ||
} | ||
|
||
function paginate<Type>(array: Type[], page: number, perPage: number): Type[] { | ||
const startIdx = (page - 1) * perPage; | ||
const endIdx = perPage * page - 1; | ||
return array.slice(startIdx, endIdx); | ||
} | ||
|
||
const MemberOfUserGroups = (props: MemberOfUserGroupsProps) => { | ||
const [groupsNamesSelected, setGroupsNamesSelected] = React.useState< | ||
string[] | ||
>([]); | ||
|
||
const [page, setPage] = React.useState(1); | ||
const [perPage, setPerPage] = React.useState(10); | ||
|
||
const [searchValue, setSearchValue] = React.useState(""); | ||
|
||
const [usersGroupsFromUser] = React.useState<UserGroup[]>( | ||
userGroupsInitialData | ||
); | ||
|
||
const [membershipDirection, setMembershipDirection] = | ||
React.useState<MembershipDirection>("direct"); | ||
|
||
// Computed "states" | ||
const someItemSelected = groupsNamesSelected.length > 0; | ||
const shownUserGroups = paginate(usersGroupsFromUser, page, perPage); | ||
const showTableRows = usersGroupsFromUser.length > 0; | ||
|
||
return ( | ||
<> | ||
<MemberOfToolbarUserGroups | ||
searchText={searchValue} | ||
onSearchTextChange={setSearchValue} | ||
refreshButtonEnabled={true} | ||
deleteButtonEnabled={someItemSelected} | ||
onDeleteButtonClick={props.showDeleteModal} | ||
addButtonEnabled={true} | ||
onAddButtonClick={props.showAddModal} | ||
membershipDirectionEnabled={true} | ||
membershipDirection={membershipDirection} | ||
onMembershipDirectionChange={setMembershipDirection} | ||
helpIconEnabled={true} | ||
totalItems={usersGroupsFromUser.length} | ||
perPage={perPage} | ||
page={page} | ||
onPerPageChange={setPerPage} | ||
onPageChange={setPage} | ||
/> | ||
<MemberOfUserGroupsTable | ||
userGroups={shownUserGroups} | ||
checkedItems={groupsNamesSelected} | ||
onCheckItemsChange={setGroupsNamesSelected} | ||
showTableRows={showTableRows} | ||
/> | ||
<Pagination | ||
className="pf-v5-u-pb-0 pf-v5-u-pr-md" | ||
itemCount={usersGroupsFromUser.length} | ||
widgetId="pagination-options-menu-bottom" | ||
perPage={perPage} | ||
page={page} | ||
variant={PaginationVariant.bottom} | ||
onSetPage={(_e, page) => setPage(page)} | ||
onPerPageSelect={(_e, perPage) => setPerPage(perPage)} | ||
/> | ||
</> | ||
); | ||
}; | ||
|
||
export default MemberOfUserGroups; |
Oops, something went wrong.