Skip to content

Commit

Permalink
Refactor 'User Groups' tabs
Browse files Browse the repository at this point in the history
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
carma12 committed Feb 12, 2024
1 parent a676ccb commit efa00cf
Show file tree
Hide file tree
Showing 5 changed files with 440 additions and 130 deletions.
107 changes: 107 additions & 0 deletions src/components/MemberOf/MemberOfTableUserGroups.tsx
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>
);
}
192 changes: 192 additions & 0 deletions src/components/MemberOf/MemberOfToolbar.tsx
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;
86 changes: 86 additions & 0 deletions src/components/MemberOf/MemberOfUserGroups.tsx
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;
Loading

0 comments on commit efa00cf

Please sign in to comment.